fix: look up referencing documents in deleteSetNull instead of trusting the relationship value - #979
Conversation
…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>
|
Warning Review limit reachedNext included review available in 54 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository: utopia-php/database/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
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. Comment |
|
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 — anddeleteDocuments()passes the caller's queries straight tofind(), where aselectquery turns relationship population off. The document then reachesdeleteSetNull()with the relationship attribute missing entirely:foreach () argument must be of type array|object, null given→ HTTP 500general_unknownempty($value)early return and silently left the other side pointing at a deleted rowThis queries the referencing documents instead — the way
deleteCascade()anddeleteRestrict()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.
$valueis 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-oneempty()return above.testManyToOneSetNullClearsUnreadableReferencescovers 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[], nevernull. I instrumented both branches and ran all 118 relationship tests —$valueis 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:
maintestOneToManySetNullAfterSelectDeleteforeach () argument must be of type array|object, null giventestManyToOneSetNullAfterSelectDeleteforeach () argument must be of type array|object, null giventestOneToOneSetNullAfterSelectDeletetestManyToOneSetNullClearsUnreadableReferencesAll four pass here.
MariaDBTest: 689 tests, 11593 assertions. The only failure istestCacheFallback, which greps for autopia-rediscontainer; I had renamed containers to isolate my stack from another checkout, and it fails identically on unmodifiedmainunder the same setup.testDeleteCollectionDeletesRelationshipserror is a pre-existing--filterordering artifact, identical onmain).phpstan --level 7 srcclean.pint --testonDatabase.phpfails identically on unmodifiedmain, so I left the file's existing formatting alone rather than reformat ~2000 unrelated lines.Related