Skip to content

Bug update document uid with delete#891

Merged
abnegate merged 16 commits into
mainfrom
bug-update-document-uid-with-delete
Jun 16, 2026
Merged

Bug update document uid with delete#891
abnegate merged 16 commits into
mainfrom
bug-update-document-uid-with-delete

Conversation

@fogelito

@fogelito fogelito commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • Bug Fixes
    • Fixed document permission records when updating a document identifier: permission entries are now properly migrated to the new identifier and removed from the old one.
    • Permission updates are no longer skipped when an update changes document identity, ensuring permissions stay in sync with the latest document.
  • Tests
    • Added end-to-end coverage verifying that changing a document’s ID (with or without permission changes) updates permission lookups and query results correctly.

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Database::updateDocument is extended to force $skipPermissionsUpdate = false when the incoming payload carries a different $id. MariaDB::updateDocument replaces the incremental diff-based permission sync with a full delete-then-insert rewrite, removes the :_newUid bind, and includes _uid directly in $columns. A new e2e test validates _perms row migration for both ID-only and ID+permissions changes.

Changes

Permission row migration on document $id change

Layer / File(s) Summary
Force permission update when $id changes
src/Database/Database.php
updateDocument sets $skipPermissionsUpdate = false when the payload '$id' differs from the current document $id, extending the existing permissions-equality skip logic.
MariaDB full permission delete-then-insert rewrite
src/Database/Adapter/MariaDB.php
Replaces diff-based remove/add permission logic with a full rewrite: adds _uid to the attribute list, deletes all _perms rows for the old UID, inserts the complete permission set under the new UID, and removes the now-unused :_newUid bind and SET _uid SQL fragment.
E2E tests for _perms migration on $id change
tests/e2e/Adapter/Scopes/DocumentTests.php
Adds testUpdateDocumentChangeIdMigratesPermissions() covering rename-only and rename+permissions-change scenarios, asserting old-ID resolution failure, find() correctness, and role-scoped permission visibility.

Sequence Diagram

sequenceDiagram
  participant Caller
  participant Database_updateDocument
  participant MariaDB_updateDocument
  participant _perms_table

  Caller->>Database_updateDocument: updateDocument($id, $document)
  Database_updateDocument->>Database_updateDocument: $id differs? → skipPermissionsUpdate = false
  Database_updateDocument->>MariaDB_updateDocument: updateDocument(collection, document, skipPermissions=false)
  MariaDB_updateDocument->>MariaDB_updateDocument: set _uid in attributes from document→getId()
  MariaDB_updateDocument->>_perms_table: DELETE WHERE _uid = oldUID
  MariaDB_updateDocument->>_perms_table: INSERT full permission set for newUID
  MariaDB_updateDocument->>MariaDB_updateDocument: UPDATE document row (SET clause from $columns, no :_newUid bind)
  MariaDB_updateDocument-->>Caller: updated document
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • utopia-php/database#611: Introduced the adapter-level $skipPermissions permission-update control that this PR extends with ID-change awareness.
  • utopia-php/database#641: Changes the default initialization of $skipPermissionsUpdate in Database::updateDocument, directly adjacent to the condition this PR adds.
  • utopia-php/database#631: Modifies permission synchronization inside updateDocument, extending prior work on controlling when permission rows are updated during document updates.

Suggested reviewers

  • abnegate

Poem

🐇 Hop, hop, the old ID's gone away,
Permissions rewritten fresh today.
Delete the old rows, insert the new,
The _perms table gleams good as new!
No diff-based diffs, just clean and bright—
The rabbit rewrote it all just right. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Bug update document uid with delete' directly describes the main change: updating document UID handling with a delete operation in the permission update logic.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bug-update-document-uid-with-delete

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/Database/Adapter/MariaDB.php (1)

1020-1020: 💤 Low value

Consider using array_keys() to avoid unused variable warning.

The $_ variable is intentionally unused since only the index is needed for placeholder generation, but this triggers static analysis warnings. Using array_keys() would be more explicit.

♻️ Suggested refactor
-                    foreach ($document->getPermissionsByType($type) as $i => $_) {
+                    foreach (array_keys($document->getPermissionsByType($type)) as $i) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Database/Adapter/MariaDB.php` at line 1020, The foreach loop iterating
over getPermissionsByType($type) uses an unused placeholder variable $_ which
triggers static analysis warnings. Replace this loop to use
array_keys($document->getPermissionsByType($type)) instead, which will iterate
only over the keys/indices without requiring an unused value variable. This
makes the intent clearer and eliminates the static analysis warning while
keeping the same logic for placeholder generation.

Source: Linters/SAST tools

tests/e2e/Adapter/Scopes/DocumentTests.php (1)

1668-1677: ⚡ Quick win

Ensure cleanup always runs (even when assertions fail).

Line 1673–Line 1677 cleanup is success-path only. If any assertion above fails, roles/collection can leak and make later tests flaky. Please wrap the test body in try/finally and move role removal + collection deletion to finally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/Adapter/Scopes/DocumentTests.php` around lines 1668 - 1677, The
cleanup code that removes roles and deletes the collection (the removeRole calls
for alice and bob, and the deleteCollection call) is only executed on the
success path. Wrap the entire test body in a try/finally block and move all
cleanup operations (both removeRole invocations and the deleteCollection call
within the skip callback) to the finally block to ensure they always execute
regardless of whether any assertion fails, preventing test pollution and
flakiness.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/Database/Adapter/MariaDB.php`:
- Line 1020: The foreach loop iterating over getPermissionsByType($type) uses an
unused placeholder variable $_ which triggers static analysis warnings. Replace
this loop to use array_keys($document->getPermissionsByType($type)) instead,
which will iterate only over the keys/indices without requiring an unused value
variable. This makes the intent clearer and eliminates the static analysis
warning while keeping the same logic for placeholder generation.

In `@tests/e2e/Adapter/Scopes/DocumentTests.php`:
- Around line 1668-1677: The cleanup code that removes roles and deletes the
collection (the removeRole calls for alice and bob, and the deleteCollection
call) is only executed on the success path. Wrap the entire test body in a
try/finally block and move all cleanup operations (both removeRole invocations
and the deleteCollection call within the skip callback) to the finally block to
ensure they always execute regardless of whether any assertion fails, preventing
test pollution and flakiness.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a9e7d0d2-b1db-4ba1-82f2-5116dec5df7c

📥 Commits

Reviewing files that changed from the base of the PR and between 6989524 and f8a17ad.

📒 Files selected for processing (3)
  • src/Database/Adapter/MariaDB.php
  • src/Database/Database.php
  • tests/e2e/Adapter/Scopes/DocumentTests.php

@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a longstanding bug where changing a document's $id during an update orphaned its permission rows: the _perms table entries remained under the old UID while the document itself was renamed, leaving the document invisible to any query that joins _perms for authorization.

  • MariaDB / Postgres / SQLite adapters: The diff-based permission update (read → compute additions/removals → apply delta) is replaced with a simpler delete-all-then-insert-all strategy. The old $id's rows are deleted and all current permissions are inserted under the new $id. _uid is now also included in the $attributes map so the main UPDATE statement handles the rename without a separate :_newUid bind.
  • Database.php: Adds a guard that forces $skipPermissionsUpdate = false whenever the document's $id changes, ensuring the adapters migrate permissions even when the permission set itself is unchanged. Also preserves the immutable $sequence field across updates.
  • Tests: A new end-to-end test covers both an ID-only rename and a combined ID + permission-set change, verifying that find() (which joins _perms) returns the document under its new UID in both cases.

Confidence Score: 5/5

Safe to merge — the fix correctly migrates permission rows when a document's UID changes, all three adapters are consistently updated, and the new E2E test directly exercises both the ID-only and combined ID+permissions scenarios.

The delete-all + insert-all permission strategy is logically equivalent to the old diff-based approach for end state, the UNIQUE constraint on _perms and Document::getPermissions() returning array_unique values together prevent duplicate rows, and the withTransaction wrapper preserves atomicity. No data-loss or incorrect-state path was found in the changed code.

No files require special attention; all three adapters follow an identical pattern and the logic is straightforward to audit.

Important Files Changed

Filename Overview
src/Database/Database.php Added UID-change detection to force permission migration, and preserves the immutable $sequence field across updates
src/Database/Adapter/MariaDB.php Replaced diff-based permission update with DELETE-old-uid + INSERT-new-uid; moves _uid into the attributes dict so the main UPDATE statement handles the rename
src/Database/Adapter/Postgres.php Parallel refactor to MariaDB: same DELETE-all + INSERT-all pattern and _uid moved into attributes for the UPDATE
src/Database/Adapter/SQLite.php Parallel refactor to MariaDB/Postgres with the same logic; also updates a PHPDoc return-type annotation for FTS schema indexes
tests/e2e/Adapter/Scopes/DocumentTests.php New E2E test covers two scenarios: ID-only rename migrates permissions, and simultaneous ID + permission change correctly reassigns perms to the new UID

Reviews (11): Last reviewed commit: "Sequence is immutable" | Re-trigger Greptile

@fogelito fogelito mentioned this pull request Jun 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Database/Adapter/MariaDB.php (1)

979-990: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the same $newUid fallback for the main row _uid.

Line 989 preserves the old-id fallback for permission rows, but Line 983 writes _uid from $document->getId() unconditionally. If the update document does not carry $id, the document row and _perms rows can be written under different UIDs.

Proposed fix
             $attributes = $document->getAttributes();
             $attributes['_createdAt'] = $document->getCreatedAt();
             $attributes['_updatedAt'] = $document->getUpdatedAt();
             $attributes['_permissions'] = json_encode($document->getPermissions());
-            $attributes['_uid'] = $document->getId();
+            $newUid = $document->offsetExists('$id') ? $document->getId() : $id;
+            $attributes['_uid'] = $newUid;
 
             $name = $this->filter($collection);
             $columns = '';
 
             if (!$skipPermissions) {
-                $newUid = $document->offsetExists('$id') ? $document->getId() : $id;
-
                 $sql = "
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Database/Adapter/MariaDB.php` around lines 979 - 990, The _uid attribute
assigned to the main document row on line 983 unconditionally uses
$document->getId(), while the $newUid fallback for permission rows on lines
989-990 conditionally falls back to the original $id if the update document
lacks an $id offset. This creates an inconsistency where the main row and
permission rows can be written under different UIDs. Modify line 983 to use the
same conditional fallback logic as $newUid, checking if the document has an $id
offset before assigning the _uid attribute, so both the main row and permission
rows always use the same UID.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Database/Adapter/MariaDB.php`:
- Around line 1008-1011: The permission value being bound in the foreach loop
over getPermissionsByType() is not being normalized before assignment to the
binds array. Apply the same normalization that createDocument() applies to
permission values (stripping double-quote characters) to the $permission
variable before binding it to $binds[":_add_{$type}_{$i}"] so that updated
permission rows match those created by the adapter and authorization lookups
function correctly.

---

Outside diff comments:
In `@src/Database/Adapter/MariaDB.php`:
- Around line 979-990: The _uid attribute assigned to the main document row on
line 983 unconditionally uses $document->getId(), while the $newUid fallback for
permission rows on lines 989-990 conditionally falls back to the original $id if
the update document lacks an $id offset. This creates an inconsistency where the
main row and permission rows can be written under different UIDs. Modify line
983 to use the same conditional fallback logic as $newUid, checking if the
document has an $id offset before assigning the _uid attribute, so both the main
row and permission rows always use the same UID.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6895b94a-6a0a-44c8-b6cc-2bce73e80549

📥 Commits

Reviewing files that changed from the base of the PR and between e5a4946 and a77ed3e.

📒 Files selected for processing (1)
  • src/Database/Adapter/MariaDB.php

Comment thread src/Database/Adapter/MariaDB.php
@abnegate abnegate merged commit cfba533 into main Jun 16, 2026
22 checks passed
@abnegate abnegate deleted the bug-update-document-uid-with-delete branch June 16, 2026 07:17
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.

2 participants