Skip to content

Conversation

@abnegate
Copy link
Member

@abnegate abnegate commented Aug 11, 2025

Summary by CodeRabbit

  • Bug Fixes

    • More reliable cache invalidation after creating, updating, deleting or renaming collections, attributes, indexes, and relationships, reducing stale data in the UI.
    • Caches are now cleared consistently even when secondary triggers fail, preventing inconsistent or partial states.
    • Document purge behavior adjusted to avoid emitting internal purge events, improving stability of cache cleanup.
  • Refactor

    • Internal flow reorganized to make cache cleanup deterministic across mutation paths.

cursoragent and others added 5 commits August 11, 2025 07:32
…teDocument don't handle it

- Remove unnecessary cache clearing from methods that call updateDocument/deleteDocument
- Only keep explicit cache clearing where actually needed
- deleteCollection: Keep purgeCachedCollection since deleteDocument handles document cache
- All other schema operations rely on updateDocument cache clearing
- Ensures purgeCachedCollection is called even if trigger fails
- This prevents cache staleness when collection is deleted from adapter but trigger throws exception
- createCollection: Clear collection and metadata document cache if trigger fails after adapter changes
- renameAttribute: Clear collection cache if trigger fails after adapter rename
- renameIndex: Clear collection cache if trigger fails after adapter rename

These prevent cache staleness when adapter operations succeed but subsequent triggers throw exceptions.
- createAttribute: Clear collection cache if trigger fails after adapter creates attribute
- deleteAttribute: Clear collection cache if trigger fails after adapter deletes attribute
- createIndex: Clear collection cache if trigger fails after adapter creates index
- deleteIndex: Clear collection cache if trigger fails after adapter deletes index

These operations all follow the pattern: adapter change -> updateDocument -> trigger.
If trigger fails, the collection cache could be stale even though updateDocument clears the metadata document cache.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 11, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Centralizes cache invalidation in Database.php by adding try/finally blocks and per-operation success flags across mutating methods so purge calls run deterministically after adapter mutations and triggers; public method signatures remain unchanged.

Changes

Cohort / File(s) Summary of changes
Database mutation control & cache finalization
src/Database/Database.php
Added try/finally around mutating operations and introduced per-operation flags (e.g., $created, $deleted, $metadataModified, $relationshipDeleted). Centralized conditional cache purges (e.g., purgeCachedCollection, purgeCachedDocument) into finally blocks across collection/attribute/index/relationship create/update/delete/rename flows. Removed document-level purge event emission in purgeCachedDocument. Minor whitespace/structural edits; no public signature changes.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Database
  participant Adapter
  participant Triggers
  participant Cache

  Client->>Database: createCollection(params)
  activate Database
  Note over Database: try { adapter mutates -> triggers run }
  Database->>Adapter: createCollection(...)
  Adapter-->>Database: result / exception
  Database->>Triggers: onAfterCreateCollection(...)
  Triggers-->>Database: result / exception
  Note over Database: finally { purge caches if $created or $metadataModified }
  Database->>Cache: purgeCachedCollection(collectionId)
  Database->>Cache: purgeCachedDocument(METADATA, collectionId)
  deactivate Database
  Database-->>Client: response / error
Loading
sequenceDiagram
  participant Client
  participant Database
  participant Adapter
  participant Triggers
  participant Cache

  Client->>Database: deleteRelationship(colA, colB, relId)
  activate Database
  Note over Database: try { adapter deletes -> triggers run }
  Database->>Adapter: deleteRelationship(...)
  Adapter-->>Database: success / exception
  Database->>Triggers: onAfterDeleteRelationship(...)
  Triggers-->>Database: result / exception
  Note over Database: finally { if $relationshipDeleted purge both collections' caches }
  Database->>Cache: purgeCachedCollection(colA)
  Database->>Cache: purgeCachedCollection(colB)
  deactivate Database
  Database-->>Client: response / error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

Thump—into the finally den I hop,
Flags held firm, I guard the cache and crop.
Create, rename, or softly delete,
I tidy traces with a rhythmic beat.
A rabbit’s code, precise and light. 🐇

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-cache-clears-in-finally-block

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🔭 Outside diff range comments (1)
src/Database/Database.php (1)

2699-2756: Missing finally + misleading comment: caches aren’t cleared for relationship updates

This block states “Cache clearing is handled by the finally block,” but the method lacks any try/finally. Relationship updates may modify both sides’ metadata (and a junction collection for many-to-many) and rename indexes; deterministic purging is needed for:

  • the parent collection,
  • the related collection,
  • the junction collection for many-to-many relations.

Proposed approach (illustrative; wrap the mutation path and purge both sides deterministically):

// Near the beginning of updateRelationship(), after resolving $collection, $attribute, $type, $side, and $relatedCollection:
$collectionIdToPurge = $collection->getId();
$relatedCollectionIdToPurge = $relatedCollection->getId();
$typeToPurge = $type;
$sideToPurge = $side;

try {
    // ... existing mutation logic (updateAttributeMeta calls, adapter->updateRelationship, index renames) ...
    return true;
} finally {
    // Ensure caches are cleared even if triggers fail
    $this->purgeCachedCollection($collectionIdToPurge);
    $this->purgeCachedCollection($relatedCollectionIdToPurge);
    if ($typeToPurge === self::RELATION_MANY_TO_MANY) {
        $junction = $this->getJunctionCollection($collection, $relatedCollection, $sideToPurge);
        $this->purgeCachedCollection($junction);
    }
}

Also, update or remove the misleading in-line comment stating that a finally block handles cache clearing.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39c6140 and a05eaf2.

📒 Files selected for processing (1)
  • src/Database/Database.php (10 hunks)
🔇 Additional comments (5)
src/Database/Database.php (5)

1322-1333: Good: finally-based purge only when the collection document was created

The try/finally structure is correct. Purging on successful metadata creation via createdCollection !== null aligns with the PR objective. No change needed.


1525-1542: Good: deterministic purge on collection deletion

Unconditional purge in finally ensures caches are cleared even if the trigger fails. Since the metadata document deletion path purges its own cache, this is consistent. No change needed.


2337-2362: Good: purge gated by successful adapter delete; NotFound path handled

The flow updates metadata only after a successful adapter delete and purges in finally when $deleted is true. The NotFound case returns true without persisting metadata and skips purge, which is consistent. No change needed.


2430-2446: Good: rename attribute purges collection in finally

Unconditional purge in finally ensures determinism even if downstream triggers fail. LGTM.


3013-3026: Good: rename index purges collection in finally

This is consistent with the objective; deterministic purge even if triggers fail. LGTM.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
src/Database/Database.php (4)

1597-1630: Create attribute: migration/duplicate path updates metadata but skips purge (stale cache risk)

If DuplicateException is swallowed during migration, you persist metadata and emit the event, but $created stays false so finally won’t purge. Add a secondary flag and purge on either success condition.

Apply this minimal diff:

-        $created = false;
+        $created = false;
+        $shouldPurge = false;
         try {
             $created = $this->adapter->createAttribute($collection->getId(), $id, $type, $size, $signed, $array);
@@
-            if ($collection->getId() !== self::METADATA) {
-                $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
-            }
+            if ($collection->getId() !== self::METADATA) {
+                $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
+                $shouldPurge = true;
+            }
@@
-            if ($collection->getId() !== self::METADATA) {
-                $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
-            }
+            if ($collection->getId() !== self::METADATA) {
+                $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
+                $shouldPurge = true;
+            }
@@
-            if ($created) {
+            if ($created || $shouldPurge) {
                 $this->purgeCachedCollection($collection->getId());
             }

1715-1750: Create attributes (batch): migration/duplicate path can skip purge; gate purge on metadata persistence too

Same issue as single-attribute create: you update metadata + fire event in the DuplicateException branch but $created remains false, so finally won't purge the collection cache.

Proposed patch:

-        $created = false;
+        $created = false;
+        $shouldPurge = false;
         try {
             $created = $this->adapter->createAttributes($collection->getId(), $attributes);
@@
-            if ($collection->getId() !== self::METADATA) {
-                $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
-            }
+            if ($collection->getId() !== self::METADATA) {
+                $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
+                $shouldPurge = true;
+            }
@@
-            if ($collection->getId() !== self::METADATA) {
-                $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
-            }
+            if ($collection->getId() !== self::METADATA) {
+                $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
+                $shouldPurge = true;
+            }
@@
-            if ($created) {
+            if ($created || $shouldPurge) {
                 $this->purgeCachedCollection($collection->getId());
                 $this->purgeCachedDocument(self::METADATA, $collection->getId());
             }

3160-3193: Create index: migration/duplicate path can skip purge like attributes

Metadata is updated and event fired in DuplicateException migration branch, but $created remains false, so finally won’t purge the collection cache. Track a secondary flag and purge on either success.

Suggested change:

-        $created = false;
+        $created = false;
+        $shouldPurge = false;
         try {
             $created = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes, $lengths, $orders, $indexAttributesWithTypes);
@@
-            if ($collection->getId() !== self::METADATA) {
-                $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
-            }
+            if ($collection->getId() !== self::METADATA) {
+                $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
+                $shouldPurge = true;
+            }
@@
-            if ($collection->getId() !== self::METADATA) {
-                $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
-            }
+            if ($collection->getId() !== self::METADATA) {
+                $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
+                $shouldPurge = true;
+            }
@@
-            if ($created) {
+            if ($created || $shouldPurge) {
                 $this->purgeCachedCollection($collection->getId());
             }

3222-3240: Delete index: metadata update + event fire even when adapter delete fails

If the adapter returns false, you still persist metadata and emit EVENT_INDEX_DELETE, and finally won’t purge. Throw on failure and only persist metadata + trigger on success.

Apply this diff:

-        $deleted = false;
+        $deleted = false;
         try {
-            $deleted = $this->adapter->deleteIndex($collection->getId(), $id);
+            $deleted = $this->adapter->deleteIndex($collection->getId(), $id);
+            if (!$deleted) {
+                throw new DatabaseException('Failed to delete index');
+            }
 
             $collection->setAttribute('indexes', \array_values($indexes));
 
             if ($collection->getId() !== self::METADATA) {
                 $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
             }
 
             $this->trigger(self::EVENT_INDEX_DELETE, $indexDeleted);
 
             return $deleted;
         } finally {
             // Ensure collection cache is cleared even if trigger fails after adapter changes
             if ($deleted) {
                 $this->purgeCachedCollection($collection->getId());
             }
         }
🧹 Nitpick comments (2)
src/Database/Database.php (2)

2713-2713: Fix Pint error: whitespace on blank line(s)

Pint is failing with no_whitespace_in_blank_line. There are whitespace-only blank lines introduced (e.g., Lines 1723, 2713). Strip trailing spaces on blank lines.

Example fix:

-            
+

Please run Pint locally to confirm:

  • php -d memory_limit=2G ./vendor/bin/pint

Also applies to: 1723-1723


2770-2771: Misleading comment: there is no finally block here

“Cache clearing is handled by the finally block” doesn’t apply in this scope. Purge occurs in downstream calls (e.g., renameIndex has its own finally; updateDocument purges metadata). Adjust the comment or add a small try/finally here if you want to standardize the pattern.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a05eaf2 and 7c68d36.

📒 Files selected for processing (1)
  • src/Database/Database.php (11 hunks)
🧰 Additional context used
🪛 GitHub Actions: Linter
src/Database/Database.php

[error] 1-1: Pint lint failed due to PSR-12 violation: no_whitespace_in_blank_line in src/Database/Database.php. Command: 'php -d memory_limit=2G ./vendor/bin/pint --test'

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (MySQL)
🔇 Additional comments (4)
src/Database/Database.php (4)

1322-1333: createCollection: finally-based dual cache purge looks correct

Good: purge both the collection and corresponding metadata document on success; guarded in finally to survive trigger failures.


1525-1542: deleteCollection: unconditional purge in finally is appropriate

Given the adapter delete is already attempted, clearing the collection cache regardless is reasonable to avoid stale entries.


2445-2461: renameAttribute: finally-based purge is consistent with the PR’s pattern

Good shift to finally; even if triggers fail after adapter change, collection cache is cleared.


3028-3041: renameIndex: finally-based purge is correct

Good move to clear cache regardless of downstream trigger failures.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
src/Database/Database.php (3)

1718-1756: createAttributes: mirrors attribute flow and purges metadata doc — LGTM

The additional purge of the metadata document is fine, even if redundant in some paths.


2720-2872: updateRelationship: finalization clears both collections — LGTM

The try/finally ensures deterministic invalidation across both collections. Acknowledging potential redundancy, per prior guidance to keep redundant purges.


3253-3271: deleteIndex: persist/trigger happen even when adapter delete fails; add failure guard

Metadata and event emit even if $deleted is false, and finally purges only when $deleted is true. This diverges state and can leave caches stale. Align with other mutators: throw on failure before persisting metadata and emitting events.

         $deleted = false;
         try {
             $deleted = $this->adapter->deleteIndex($collection->getId(), $id);
+            if (!$deleted) {
+                throw new DatabaseException('Failed to delete index');
+            }
 
             $collection->setAttribute('indexes', \array_values($indexes));
 
             if ($collection->getId() !== self::METADATA) {
                 $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
             }
 
             $this->trigger(self::EVENT_INDEX_DELETE, $indexDeleted);
 
             return $deleted;
         } finally {
             // Ensure collection cache is cleared even if trigger fails after adapter changes
             if ($deleted) {
                 $this->purgeCachedCollection($collection->getId());
             }
         }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c68d36 and 6dec65c.

📒 Files selected for processing (1)
  • src/Database/Database.php (11 hunks)
🧰 Additional context used
🪛 GitHub Actions: Linter
src/Database/Database.php

[error] 1-1: PSR-12: no_whitespace_in_blank_line violation detected by Pint in this file.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Adapter Tests (Pool)
🔇 Additional comments (7)
src/Database/Database.php (7)

1322-1333: Deterministic purge in finally for createCollection — LGTM

Good use of finally with a success sentinel; purging both the collection and its metadata document is correct.


1525-1542: Unconditional purge after adapter delete — LGTM

Clearing collection cache in finally regardless of trigger outcome matches the PR goal and avoids stale state.


1597-1633: createAttribute: success/metadata flags + finally purge — LGTM

Gating purge on either adapter success or metadata update handles migration/duplicate paths correctly.


2925-3010: deleteRelationship: success flag + finally purge for both collections — LGTM

The relationshipDeleted flag protects against unnecessary clears while ensuring consistency when deletion completes.


3056-3069: renameIndex: finally-based purge — LGTM

Purge on exit aligns with the centralized invalidation approach.


3188-3224: createIndex: success/metadata flags + finally purge — LGTM

Covers duplicate/migration paths and ensures purge when metadata changed even if adapter path differs.


1-2: Fix Pint PSR-12 lint error: whitespace in blank line

The linter reports “no_whitespace_in_blank_line” for this file. Please run Pint and commit formatting fixes.

You can verify and fix locally with:

  • vendor/bin/pint -v
  • Or php-cs-fixer equivalent if you use it in this repo.

Comment on lines +2358 to +2383
$deleted = false;
try {
if (!$this->adapter->deleteAttribute($collection->getId(), $id)) {
$deleted = $this->adapter->deleteAttribute($collection->getId(), $id);
if (!$deleted) {
throw new DatabaseException('Failed to delete attribute');
}
} catch (NotFoundException) {
// Ignore
}

$collection->setAttribute('attributes', \array_values($attributes));
$collection->setAttribute('indexes', \array_values($indexes));

if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}
$collection->setAttribute('attributes', \array_values($attributes));
$collection->setAttribute('indexes', \array_values($indexes));

$this->purgeCachedCollection($collection->getId());
$this->purgeCachedDocument(self::METADATA, $collection->getId());
if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}

$this->trigger(self::EVENT_ATTRIBUTE_DELETE, $attribute);
$this->trigger(self::EVENT_ATTRIBUTE_DELETE, $attribute);

return true;
return true;
} catch (NotFoundException) {
// Ignore - attribute doesn't exist
return true;
} finally {
// Ensure collection cache is cleared even if trigger fails after adapter changes
if ($deleted) {
$this->purgeCachedCollection($collection->getId());
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Idempotent deleteAttribute should still persist metadata and purge caches on NotFound

Currently, catching NotFound returns early without persisting metadata changes or purging, leaving metadata/caches stale if the attribute exists only in metadata. Treat NotFound as idempotent success: persist the metadata removal, fire the event, mark as deleted to ensure purge in finally.

         } catch (NotFoundException) {
-            // Ignore - attribute doesn't exist
-            return true;
+            // Idempotent: persist metadata and clear caches even if adapter had nothing to delete
+            $collection->setAttribute('attributes', \array_values($attributes));
+            $collection->setAttribute('indexes', \array_values($indexes));
+            if ($collection->getId() !== self::METADATA) {
+                $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
+            }
+            $this->trigger(self::EVENT_ATTRIBUTE_DELETE, $attribute);
+            $deleted = true; // ensure finally purges
+            return true;
         } finally {
             // Ensure collection cache is cleared even if trigger fails after adapter changes
             if ($deleted) {
                 $this->purgeCachedCollection($collection->getId());
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$deleted = false;
try {
if (!$this->adapter->deleteAttribute($collection->getId(), $id)) {
$deleted = $this->adapter->deleteAttribute($collection->getId(), $id);
if (!$deleted) {
throw new DatabaseException('Failed to delete attribute');
}
} catch (NotFoundException) {
// Ignore
}
$collection->setAttribute('attributes', \array_values($attributes));
$collection->setAttribute('indexes', \array_values($indexes));
if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}
$collection->setAttribute('attributes', \array_values($attributes));
$collection->setAttribute('indexes', \array_values($indexes));
$this->purgeCachedCollection($collection->getId());
$this->purgeCachedDocument(self::METADATA, $collection->getId());
if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}
$this->trigger(self::EVENT_ATTRIBUTE_DELETE, $attribute);
$this->trigger(self::EVENT_ATTRIBUTE_DELETE, $attribute);
return true;
return true;
} catch (NotFoundException) {
// Ignore - attribute doesn't exist
return true;
} finally {
// Ensure collection cache is cleared even if trigger fails after adapter changes
if ($deleted) {
$this->purgeCachedCollection($collection->getId());
}
}
$deleted = false;
try {
$deleted = $this->adapter->deleteAttribute($collection->getId(), $id);
if (!$deleted) {
throw new DatabaseException('Failed to delete attribute');
}
$collection->setAttribute('attributes', \array_values($attributes));
$collection->setAttribute('indexes', \array_values($indexes));
if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}
$this->trigger(self::EVENT_ATTRIBUTE_DELETE, $attribute);
return true;
} catch (NotFoundException) {
// Idempotent: persist metadata and clear caches even if adapter had nothing to delete
$collection->setAttribute('attributes', \array_values($attributes));
$collection->setAttribute('indexes', \array_values($indexes));
if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}
$this->trigger(self::EVENT_ATTRIBUTE_DELETE, $attribute);
$deleted = true; // ensure finally purges
return true;
} finally {
// Ensure collection cache is cleared even if trigger fails after adapter changes
if ($deleted) {
$this->purgeCachedCollection($collection->getId());
}
}
🤖 Prompt for AI Agents
In src/Database/Database.php around lines 2358 to 2383, the NotFound catch
returns early so metadata updates and cache purge are skipped; change the catch
to treat NotFound as idempotent success by persisting the metadata changes (call
updateDocument for METADATA-as-before), firing the EVENT_ATTRIBUTE_DELETE, and
setting $deleted = true so the finally block will purge the cached collection —
then return true. Ensure the same guards (skip updateDocument when collection id
is self::METADATA) are applied and preserve existing behavior when delete
actually succeeds.

Comment on lines +2451 to +2467
try {
$renamed = $this->adapter->renameAttribute($collection->getId(), $old, $new);

$collection->setAttribute('attributes', $attributes);
$collection->setAttribute('indexes', $indexes);
$collection->setAttribute('attributes', $attributes);
$collection->setAttribute('indexes', $indexes);

if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}
if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}

$this->trigger(self::EVENT_ATTRIBUTE_UPDATE, $attribute);
$this->trigger(self::EVENT_ATTRIBUTE_UPDATE, $attribute);

return $renamed;
return $renamed;
} finally {
// Ensure collection cache is cleared even if trigger fails after adapter changes
$this->purgeCachedCollection($collection->getId());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard on adapter rename result to avoid diverging metadata

If adapter->renameAttribute fails (returns false), we should not persist metadata or emit events. Throw on failure before updating metadata; keep the finally-based purge.

         try {
             $renamed = $this->adapter->renameAttribute($collection->getId(), $old, $new);
+            if (!$renamed) {
+                throw new DatabaseException('Failed to rename attribute');
+            }
 
             $collection->setAttribute('attributes', $attributes);
             $collection->setAttribute('indexes', $indexes);
 
             if ($collection->getId() !== self::METADATA) {
                 $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
             }
 
             $this->trigger(self::EVENT_ATTRIBUTE_UPDATE, $attribute);
 
             return $renamed;
         } finally {
             // Ensure collection cache is cleared even if trigger fails after adapter changes
             $this->purgeCachedCollection($collection->getId());
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
$renamed = $this->adapter->renameAttribute($collection->getId(), $old, $new);
$collection->setAttribute('attributes', $attributes);
$collection->setAttribute('indexes', $indexes);
$collection->setAttribute('attributes', $attributes);
$collection->setAttribute('indexes', $indexes);
if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}
if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}
$this->trigger(self::EVENT_ATTRIBUTE_UPDATE, $attribute);
$this->trigger(self::EVENT_ATTRIBUTE_UPDATE, $attribute);
return $renamed;
return $renamed;
} finally {
// Ensure collection cache is cleared even if trigger fails after adapter changes
$this->purgeCachedCollection($collection->getId());
}
try {
$renamed = $this->adapter->renameAttribute($collection->getId(), $old, $new);
if (!$renamed) {
throw new DatabaseException('Failed to rename attribute');
}
$collection->setAttribute('attributes', $attributes);
$collection->setAttribute('indexes', $indexes);
if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}
$this->trigger(self::EVENT_ATTRIBUTE_UPDATE, $attribute);
return $renamed;
} finally {
// Ensure collection cache is cleared even if trigger fails after adapter changes
$this->purgeCachedCollection($collection->getId());
}
🤖 Prompt for AI Agents
In src/Database/Database.php around lines 2451 to 2467, the code proceeds to
update metadata and emit events even if $this->adapter->renameAttribute returned
false; change the flow so you first capture the adapter result into $renamed,
and if $renamed is false throw an exception (or return/fail) immediately before
mutating $collection or calling updateDocument/trigger, while leaving the
existing finally block that calls
$this->purgeCachedCollection($collection->getId()) intact.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🔭 Outside diff range comments (1)
src/Database/Database.php (1)

1-1: Remove whitespace from blank lines in Database.php

Two blank lines in src/Database/Database.php contain spaces, violating PSR-12’s no_whitespace_in_blank_line rule. Please strip the whitespace (e.g., re-run Pint or delete spaces manually):

• src/Database/Database.php:1725
• src/Database/Database.php:1740

♻️ Duplicate comments (3)
src/Database/Database.php (3)

2356-2381: deleteAttribute NotFound path returns early without persisting metadata or purging

On NotFound, you return before updating metadata and before setting $deleted, so finally won’t purge. This can leave metadata/caches stale if metadata contained the attribute but the adapter did not. Treat NotFound as idempotent success: persist metadata removal, fire the event, mark $deleted = true to ensure purge.

Apply this diff:

         } catch (NotFoundException) {
-            // Ignore - attribute doesn't exist
-            return true;
+            // Idempotent: persist metadata and clear caches even if adapter had nothing to delete
+            $collection->setAttribute('attributes', \array_values($attributes));
+            $collection->setAttribute('indexes', \array_values($indexes));
+            if ($collection->getId() !== self::METADATA) {
+                $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
+            }
+            $this->trigger(self::EVENT_ATTRIBUTE_DELETE, $attribute);
+            $deleted = true; // ensure finally purges
+            return true;
         } finally {
             // Ensure collection cache is cleared even if trigger fails after adapter changes
             if ($deleted) {
                 $this->purgeCachedCollection($collection->getId());
             }
         }

2449-2465: Guard on adapter rename result to avoid diverging metadata

If adapter->renameAttribute fails, code still persists metadata and triggers an update, diverging metadata from schema. Throw on failure before metadata changes to keep state consistent.

Apply this diff:

         try {
-            $renamed = $this->adapter->renameAttribute($collection->getId(), $old, $new);
+            $renamed = $this->adapter->renameAttribute($collection->getId(), $old, $new);
+            if (!$renamed) {
+                throw new DatabaseException('Failed to rename attribute');
+            }
 
             $collection->setAttribute('attributes', $attributes);
             $collection->setAttribute('indexes', $indexes);
 
             if ($collection->getId() !== self::METADATA) {
                 $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
             }
 
             $this->trigger(self::EVENT_ATTRIBUTE_UPDATE, $attribute);
 
             return $renamed;
         } finally {
             // Ensure collection cache is cleared even if trigger fails after adapter changes
             $this->purgeCachedCollection($collection->getId());
         }

3251-3269: deleteIndex persists metadata and triggers regardless of adapter result

Currently metadata is updated and event emitted even if $deleted is false, and finally purges only when $deleted is true. Throw on failure to keep state consistent and ensure purge reflects actual change.

Apply this diff:

-        $deleted = false;
+        $deleted = false;
         try {
             $deleted = $this->adapter->deleteIndex($collection->getId(), $id);
+            if (!$deleted) {
+                throw new DatabaseException('Failed to delete index');
+            }
 
             $collection->setAttribute('indexes', \array_values($indexes));
 
             if ($collection->getId() !== self::METADATA) {
                 $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
             }
 
             $this->trigger(self::EVENT_INDEX_DELETE, $indexDeleted);
 
             return $deleted;
         } finally {
             // Ensure collection cache is cleared even if trigger fails after adapter changes
             if ($deleted) {
                 $this->purgeCachedCollection($collection->getId());
             }
         }
🧹 Nitpick comments (1)
src/Database/Database.php (1)

2797-2809: Consider avoiding double metadata writes when renaming indexes

You first call updateIndexMeta to update index attributes, then call renameIndex, which updates metadata again and emits a separate event. This doubles metadata writes/events for the same rename intent.

If both events are not required, consider consolidating to one path (prefer renameIndex only, or extend renameIndex to accept an updated attributes list).

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6dec65c and 8522413.

📒 Files selected for processing (1)
  • src/Database/Database.php (11 hunks)
🧰 Additional context used
🪛 GitHub Actions: Linter
src/Database/Database.php

[error] 1-1: Pint lint failed (exit code 1): PSR-12 'no_whitespace_in_blank_line' violation detected in src/Database/Database.php.

🪛 GitHub Actions: Tests
src/Database/Database.php

[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: with-dash


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: active


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: director


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: director


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: price


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: price


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: director


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: price


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: price


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: year


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: year


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: price


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: price


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: name


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: name


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: director


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: director


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: active


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: active


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: nullable


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: nullable


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: name


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: name


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: price


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: name


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: name


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: price


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: name


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: name


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: price


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: name


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: name


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: price


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: name


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: name


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: price


[error] 6178-6178: Utopia\Database\Exception\Query: Invalid query: Attribute not found in schema: name


[error] 1248-1248: Utopia\Database\Exception\Duplicate: Collection indexes already exists


[error] 3770-3770: Utopia\Database\Exception\Structure: Invalid document structure: Unknown attribute: 'string'


[error] 3770-3770: Utopia\Database\Exception\Structure: Invalid document structure: Unknown attribute: 'string'

🔇 Additional comments (9)
src/Database/Database.php (9)

1322-1332: createCollection: finally-based purge looks good

Purging only when the metadata document was successfully created ($createdCollection !== null) is correct and avoids unnecessary cache churn.


1524-1541: deleteCollection: unconditional final purge aligned with intent

Good move to clear cache in finally regardless of $deleted. This matches the “adapter.deleteCollection() was called” rationale and avoids stale entries if trigger fails.


1596-1632: createAttribute: fixes stale-cache risks in both normal and migration paths

The $created + $metadataModified flags with a finally purge cover both success and DuplicateException migration flows. This addresses the prior “skip purge on migration path” risk.


1717-1754: createAttributes: mirrors robust purge semantics from createAttribute

Same solid pattern with $created and $metadataModified and a guarded finally purge.


2718-2870: updateRelationship: deterministic cache cleanup for both collections

Wrapping the entire flow and purging both involved collection caches in finally is the right call given multi-collection impact.


2923-3008: deleteRelationship: finally-based dual purge executed only on successful adapter delete

The $relationshipDeleted flag ensures finally purges both collections only when the adapter deletion succeeded. Given index deletions already purge during the metadata phase, this keeps the semantics safe. LGTM.


3186-3222: createIndex: correct purge semantics in normal and migration paths

The $created/$metadataModified flags and finally purge cover adapter-success and migration branch consistently. Nice.


6109-6117: Removing EVENT_DOCUMENT_PURGE emission: confirm no listeners rely on it

purgeCachedDocument no longer triggers EVENT_DOCUMENT_PURGE. If downstream listeners use this event, this is a behavioral change. If intentional, consider documenting; otherwise, reintroduce the trigger here or where appropriate.

Would you like me to re-add the event trigger here (non-silent) or emit a dedicated “cache_purged” event to avoid coupling?


6159-6206: Verify selections and schema mapping before merging
Ensure that attribute-based queries and selections correctly reference the collection’s schema at runtime:

  • Database::validateSelections (src/Database/Database.php around line 6734) calls getSchemaAttributes($collection->getId()) to build the list of valid attribute IDs.
  • All adapters (SQL, SQLite, MariaDB, Postgres, Pool) implement getSchemaAttributes(); confirm they return every column name, including dashed or nullable fields.
  • DocumentsValidator is invoked in Database::find/select paths (around lines 1798, 3320, 6216) to validate filters and selections.
  • Tests in tests/unit/Validator/QueryTest.php and tests/e2e/Adapter/Scopes/DocumentTests.php reference attributes “name”, “director”, “year”, “price”, “active”, “nullable”, “with-dash” – run the full suite against each adapter to catch any QueryException (“Attribute not found in schema…”).

Please manually verify that getSchemaAttributes correctly lists all attributes for each adapter and that validateSelections/ DocumentsValidator accept selections matching your intended schema.

Comment on lines +3054 to +3067
try {
$this->adapter->renameIndex($collection->getId(), $old, $new);

if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}
if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}

$this->trigger(self::EVENT_INDEX_RENAME, $indexNew);
$this->trigger(self::EVENT_INDEX_RENAME, $indexNew);

return true;
return true;
} finally {
// Ensure collection cache is cleared even if trigger fails after adapter changes
$this->purgeCachedCollection($collection->getId());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

renameIndex lacks adapter-result guard; risk of metadata/schema divergence

If the adapter rename fails, metadata is still persisted and event emitted. Gate metadata update and event emission on actual adapter success.

Apply this diff:

         try {
-            $this->adapter->renameIndex($collection->getId(), $old, $new);
+            $renamed = $this->adapter->renameIndex($collection->getId(), $old, $new);
+            if (!$renamed) {
+                throw new DatabaseException('Failed to rename index');
+            }
 
             if ($collection->getId() !== self::METADATA) {
                 $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
             }
 
             $this->trigger(self::EVENT_INDEX_RENAME, $indexNew);
 
-            return true;
+            return true;
         } finally {
             // Ensure collection cache is cleared even if trigger fails after adapter changes
             $this->purgeCachedCollection($collection->getId());
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
$this->adapter->renameIndex($collection->getId(), $old, $new);
if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}
if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}
$this->trigger(self::EVENT_INDEX_RENAME, $indexNew);
$this->trigger(self::EVENT_INDEX_RENAME, $indexNew);
return true;
return true;
} finally {
// Ensure collection cache is cleared even if trigger fails after adapter changes
$this->purgeCachedCollection($collection->getId());
}
try {
$renamed = $this->adapter->renameIndex($collection->getId(), $old, $new);
if (!$renamed) {
throw new DatabaseException('Failed to rename index');
}
if ($collection->getId() !== self::METADATA) {
$this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
}
$this->trigger(self::EVENT_INDEX_RENAME, $indexNew);
return true;
} finally {
// Ensure collection cache is cleared even if trigger fails after adapter changes
$this->purgeCachedCollection($collection->getId());
}
🤖 Prompt for AI Agents
In src/Database/Database.php around lines 3054 to 3067, the code updates
metadata and emits the rename event unconditionally after calling
$this->adapter->renameIndex; change this so the metadata update and
$this->trigger call only run when the adapter call actually succeeds: store the
result of $this->adapter->renameIndex(...), if it returns falsy return false
immediately (still run the finally purge), otherwise proceed to updateDocument
and trigger the event and then return true; keep the existing finally block
intact so the collection cache is always purged even on failure or exception.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants