From b10ed3e445dc47e198ba8066c9da9d6d72a8087e Mon Sep 17 00:00:00 2001 From: PINYO PATTANAWASANPORN Date: Mon, 21 Sep 2026 13:06:04 +0700 Subject: [PATCH] fix: query relations in ManyToOne deleteSetNull and guard empty collections (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. --- src/Database/Database.php | 18 ++++++++++++------ .../Scopes/Relationships/ManyToOneTests.php | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index d581529fa5..a6a5cc2f8d 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -8194,6 +8194,10 @@ private function deleteSetNull(Document $collection, Document $relatedCollection if ($side === Database::RELATION_SIDE_CHILD) { break; } + + if (empty($value)) { + break; + } foreach ($value as $relation) { $this->authorization->skip(function () use ($relatedCollection, $twoWayKey, $relation) { $this->skipRelationships(fn () => $this->updateDocument( @@ -8212,12 +8216,14 @@ private function deleteSetNull(Document $collection, Document $relatedCollection break; } - if (!$twoWay) { - $value = $this->find($relatedCollection->getId(), [ - Query::select(['$id']), - Query::equal($twoWayKey, [$document->getId()]), - Query::limit(PHP_INT_MAX) - ]); + $value = $this->find($relatedCollection->getId(), [ + Query::select(['$id']), + Query::equal($twoWayKey, [$document->getId()]), + Query::limit(PHP_INT_MAX) + ]); + + if (empty($value)) { + break; } foreach ($value as $relation) { diff --git a/tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php b/tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php index e62ff735c3..0e81285959 100644 --- a/tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php +++ b/tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php @@ -782,6 +782,24 @@ public function testManyToOneTwoWayRelationship(): void $database->getDocument('product', 'product1'); $this->assertEquals(null, $product1->getAttribute('newStore')); + + // Create child with no related parents and verify deleteSetNull succeeds + $database->createDocument('store', new Document([ + '$id' => 'store8', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Store 8', + 'opensAt' => '10:00', + ])); + + $deleted = $database->deleteDocument('store', 'store8'); + $this->assertEquals(true, $deleted); + + $store8 = $database->getDocument('store', 'store8'); + $this->assertEquals(true, $store8->isEmpty()); // Change on delete to cascade $database->updateRelationship( collection: 'product',