Skip to content

fix: look up referencing documents in deleteSetNull instead of trusting the relationship value - #978

Closed
HarshMN2345 wants to merge 2 commits into
mainfrom
claude/setnull-missing-relationship-value
Closed

HarshMN2345 wants to merge 2 commits into
mainfrom
claude/setnull-missing-relationship-value

Conversation

@HarshMN2345

@HarshMN2345 HarshMN2345 commented Sep 21, 2026

Copy link
Copy Markdown
Member

What

deleteSetNull() read the related documents off the document being deleted. That value is only present when the document was fetched with its relationships populated — and deleteDocuments() passes the caller's queries straight to find(), where a select query turns relationship population off. The document then reaches deleteSetNull() with the relationship attribute missing entirely:

  • one-to-many (parent side) and many-to-one (child side): foreach () argument must be of type array|object, null given → HTTP 500 general_unknown
  • one-to-one (two-way): took the empty($value) early return and silently left the other side pointing at a deleted row

This queries the referencing documents instead — the way deleteCascade() and deleteRestrict() already do — so the outcome no longer depends on how the document happened to be loaded.

The lookup runs with permissions skipped. A referencing document the caller cannot read still has to have its foreign key cleared, or deleting a row leaves dangling references whenever document security hides some of the referencing rows. That also fixes the pre-existing one-way many-to-one lookup, which queried but did so as the caller.

$value is no longer read on entry, so it is dropped from the private method's signature.

Why not just guard the empty value

Two things worth recording, because they rule out the smaller-looking fixes:

  • if (empty($value)) break; stops the crash but skips the null-out, turning a 500 into a silent dangling foreign key — the same failure mode as the one-to-one empty() return above.
  • Making the many-to-one lookup unconditional without skipping permissions is a regression. Today the populated value comes from a permission-skipped fetch, so every referencing row is cleared; running the lookup as the caller silently skips rows they cannot read. testManyToOneSetNullClearsUnreadableReferences covers that.

Note also that a plain single-row delete of a two-way many-to-one child with zero related rows already works on main: populateManyToOneRelationshipsBatch() sets the child-side attribute to [], never null. I instrumented both branches and ran all 118 relationship tests — $value is never non-array there. The null is only reachable through the bulk-delete-with-select path above, which is what the tests below exercise.

Test Plan

Four tests added, in the per-type traits:

Test On main
testOneToManySetNullAfterSelectDelete foreach () argument must be of type array|object, null given
testManyToOneSetNullAfterSelectDelete foreach () argument must be of type array|object, null given
testOneToOneSetNullAfterSelectDelete fails — foreign key left dangling
testManyToOneSetNullClearsUnreadableReferences passes — locks in the permission behaviour

All four pass here.

  • Full MariaDBTest: 689 tests, 11593 assertions. The only failure is testCacheFallback, which greps for a utopia-redis container; I had renamed containers to isolate my stack from another checkout, and it fails identically on unmodified main under the same setup.
  • Relationship suite green on MariaDB, Postgres and MongoDB (the one testDeleteCollectionDeletesRelationships error is a pre-existing --filter ordering artifact, identical on main).
  • phpstan --level 7 src clean. pint --test on Database.php fails identically on unmodified main, so I left the file's existing formatting alone rather than reformat ~2000 unrelated lines.

Related

…ctions (appwrite/appwrite#13766)

In deleteSetNull(), RELATION_MANY_TO_ONE previously wrapped the related documents lookup in `if (!$twoWay)`. When deleting a child document in a two-way Many-to-One relationship, `$this->find()` was skipped, leaving `$value` unpopulated (null) and causing a fatal `TypeError: foreach() argument must be of type array|object, null given`.

This fix aligns deleteSetNull() with deleteCascade() and deleteRestrict() by unconditionally querying the referencing parent documents, and adds defensive empty checks for RELATION_ONE_TO_MANY and RELATION_MANY_TO_ONE.
…ng the relationship value

deleteSetNull() read the related documents off the document being deleted.
That value is only there when the document was fetched with its relationships
populated. deleteDocuments() passes the caller's queries straight to find(),
and a select query turns relationship population off, so the document reaches
deleteSetNull() with the relationship attribute missing:

  - one-to-many (parent side) and many-to-one (child side) hit
    `foreach () argument must be of type array|object, null given`,
    surfacing as HTTP 500 general_unknown
  - one-to-one (two-way) took the `empty($value)` early return and silently
    left the other side pointing at a deleted row

Query the referencing documents instead, the way deleteCascade() and
deleteRestrict() already do, so the outcome no longer depends on how the
document was loaded.

The lookup runs with permissions skipped. A referencing document the caller
cannot read still has to have its foreign key cleared, otherwise deleting a
row leaves dangling references behind whenever document security hides some
of the referencing rows. This also fixes that case for the one-way
many-to-one lookup, which already queried but did so as the caller.

`$value` is no longer read on entry, so it is dropped from the signature.

Co-authored-by: PINYO PATTANAWASANPORN <pattanawasanporn@gmail.com>
@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Repository: utopia-php/database/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: a3a1b507-7438-47c1-8e8e-c7668c9ecf4c

📥 Commits

Reviewing files that changed from the base of the PR and between e45195f and f148181.

📒 Files selected for processing (4)
  • src/Database/Database.php
  • tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php
  • tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php
  • tests/e2e/Adapter/Scopes/Relationships/OneToOneTests.php
 ________________________________________________________________________________________________
< Sign your work. Craftsmen of an earlier age were proud to sign their work. You should be, too. >
 ------------------------------------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

@greptile-apps

greptile-apps Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the reverse lookups follow relationship storage direction and preserve cleanup across authorization boundaries.

Summary

This PR makes set-null deletion independent of whether relationships were populated on the document being deleted.

  • Looks up surviving documents through their persisted reverse relationship keys.
  • Performs cleanup without caller read permissions to avoid hidden dangling references.
  • Adds observable regression coverage for bulk deletes using projections and permission-hidden references.

Reviews (1) · Last reviewed commit: "fix: look up referencing documents in de..."

@HarshMN2345
HarshMN2345 deleted the claude/setnull-missing-relationship-value branch September 21, 2026 06:54
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