From 0e6ad57eeb81c2f278943840c4837c77bf2e8f6d Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 9 Jun 2026 15:53:21 +0300 Subject: [PATCH 01/16] partialUpdate --- src/Database/Database.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index f9fad6808..e78bd249b 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6159,6 +6159,7 @@ public function updateDocument(string $collection, string $id, Document $documen $skipPermissionsUpdate = ($originalPermissions === $currentPermissions); } $createdAt = $document->getCreatedAt(); + $inputKeys = \array_keys($document->getArrayCopy()); $document = \array_merge($old->getArrayCopy(), $document->getArrayCopy()); $document['$collection'] = $old->getAttribute('$collection'); // Make sure user doesn't switch collection ID @@ -6334,7 +6335,11 @@ public function updateDocument(string $collection, string $id, Document $documen $document = $this->adapter->castingBefore($collection, $document); - $this->adapter->updateDocument($collection, $id, $document, $skipPermissionsUpdate); + $internalKeys = \array_column(self::INTERNAL_ATTRIBUTES, '$id'); + $allowedKeys = \array_flip(\array_merge($inputKeys, $internalKeys)); + $adapterDocument = new Document(\array_intersect_key($document->getArrayCopy(), $allowedKeys)); + + $this->adapter->updateDocument($collection, $id, $adapterDocument, $skipPermissionsUpdate); $document = $this->adapter->castingAfter($collection, $document); From 90e3634eaef14b34a8fe97f9849de94726e6606f Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jun 2026 14:29:57 +0300 Subject: [PATCH 02/16] update --- src/Database/Adapter/MariaDB.php | 14 ++++++++++---- src/Database/Database.php | 27 +++++++++++++-------------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 599da878e..0727b3aac 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -977,9 +977,16 @@ public function updateDocument(Document $collection, string $id, Document $docum $spatialAttributes = $this->getSpatialAttributes($collection); $collection = $collection->getId(); $attributes = $document->getAttributes(); - $attributes['_createdAt'] = $document->getCreatedAt(); $attributes['_updatedAt'] = $document->getUpdatedAt(); - $attributes['_permissions'] = json_encode($document->getPermissions()); + if ($document->offsetExists('$id')) { + $attributes['_uid'] = $document->getId(); + } + if ($document->offsetExists('$createdAt')) { + $attributes['_createdAt'] = $document->getCreatedAt(); + } + if ($document->offsetExists('$permissions')) { + $attributes['_permissions'] = json_encode($document->getPermissions()); + } $name = $this->filter($collection); $columns = ''; @@ -1168,7 +1175,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $sql = " UPDATE {$this->getSQLTable($name)} - SET {$columns} _uid = :_newUid + SET " . \rtrim($columns, ',') . " WHERE _id=:_sequence {$this->getTenantQuery($collection)} "; @@ -1178,7 +1185,6 @@ public function updateDocument(Document $collection, string $id, Document $docum $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_sequence', $document->getSequence()); - $stmt->bindValue(':_newUid', $document->getId()); if ($this->sharedTables) { $stmt->bindValue(':_tenant', $this->tenant); diff --git a/src/Database/Database.php b/src/Database/Database.php index e78bd249b..c6f2604a4 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6138,6 +6138,11 @@ public function updateDocument(string $collection, string $id, Document $documen $collection = $this->silent(fn () => $this->getCollection($collection)); $newUpdatedAt = $document->getUpdatedAt(); + + $document->removeAttribute('$collection'); + $document->removeAttribute('$tenant'); + $document->removeAttribute('$sequence'); + $document = $this->withTransaction(function () use ($collection, $id, $document, $newUpdatedAt) { $time = DateTime::now(); $old = $this->authorization->skip(fn () => $this->silent( @@ -6158,19 +6163,13 @@ public function updateDocument(string $collection, string $id, Document $documen $skipPermissionsUpdate = ($originalPermissions === $currentPermissions); } - $createdAt = $document->getCreatedAt(); - $inputKeys = \array_keys($document->getArrayCopy()); - - $document = \array_merge($old->getArrayCopy(), $document->getArrayCopy()); - $document['$collection'] = $old->getAttribute('$collection'); // Make sure user doesn't switch collection ID - $document['$createdAt'] = ($createdAt === null || !$this->preserveDates) ? $old->getCreatedAt() : $createdAt; + if (!$this->preserveDates) { + $document->removeAttribute('$createdAt'); + } if ($this->adapter->getSharedTables()) { - $tenant = $old->getTenant(); - $document['$tenant'] = $tenant; - $old->setAttribute('$tenant', $tenant); // Normalize for strict comparison + $old->setAttribute('$tenant', $old->getTenant()); // Normalize for strict comparison } - $document = new Document($document); $attributes = $collection->getAttribute('attributes', []); @@ -6335,11 +6334,11 @@ public function updateDocument(string $collection, string $id, Document $documen $document = $this->adapter->castingBefore($collection, $document); - $internalKeys = \array_column(self::INTERNAL_ATTRIBUTES, '$id'); - $allowedKeys = \array_flip(\array_merge($inputKeys, $internalKeys)); - $adapterDocument = new Document(\array_intersect_key($document->getArrayCopy(), $allowedKeys)); + $document->setAttribute('$sequence', $old->getSequence()); + + $this->adapter->updateDocument($collection, $id, $document, $skipPermissionsUpdate); - $this->adapter->updateDocument($collection, $id, $adapterDocument, $skipPermissionsUpdate); + $document = new Document(\array_merge($old->getArrayCopy(), $document->getArrayCopy())); $document = $this->adapter->castingAfter($collection, $document); From 4e56486fb1481f6f6e99a70f9ed6d26c927a0731 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jun 2026 14:50:25 +0300 Subject: [PATCH 03/16] add collection --- src/Database/Database.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Database/Database.php b/src/Database/Database.php index c6f2604a4..e4a4d130b 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6311,6 +6311,8 @@ public function updateDocument(string $collection, string $id, Document $documen throw new ConflictException('Document was updated after the request timestamp'); } + $document->setAttribute('$collection', $collection->getId()); + $document = $this->encode($collection, $document); if ($this->validate) { From 8508d6331bd94240a7f833ef78036f3ee0e3b135 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jun 2026 15:13:28 +0300 Subject: [PATCH 04/16] fix validation --- src/Database/Database.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index adbf21dc1..fcc9380c8 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6311,12 +6311,10 @@ public function updateDocument(string $collection, string $id, Document $documen throw new ConflictException('Document was updated after the request timestamp'); } - $document->setAttribute('$collection', $collection->getId()); - $document = $this->encode($collection, $document); if ($this->validate) { - $structureValidator = new Structure( + $structureValidator = new PartialStructure( $collection, $this->adapter->getIdAttributeType(), $this->adapter->getMinDateTime(), @@ -6325,7 +6323,7 @@ public function updateDocument(string $collection, string $id, Document $documen supportUnsignedBigInt: $this->adapter->getSupportForUnsignedBigInt(), currentDocument: $old ); - if (!$structureValidator->isValid($document)) { // Make sure updated structure still apply collection rules (if any) + if (!$structureValidator->isValid($document)) { throw new StructureException($structureValidator->getDescription()); } } From 6b2a7b5152e8f70629e0929c10f06d23e95ca3d4 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jun 2026 15:30:53 +0300 Subject: [PATCH 05/16] remove unset --- src/Database/Database.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index fcc9380c8..19d57bd1c 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6139,10 +6139,6 @@ public function updateDocument(string $collection, string $id, Document $documen $collection = $this->silent(fn () => $this->getCollection($collection)); $newUpdatedAt = $document->getUpdatedAt(); - $document->removeAttribute('$collection'); - $document->removeAttribute('$tenant'); - $document->removeAttribute('$sequence'); - $document = $this->withTransaction(function () use ($collection, $id, $document, $newUpdatedAt) { $time = DateTime::now(); $old = $this->authorization->skip(fn () => $this->silent( From 6d4db8d3203c1b3f9413b3aadf17b94033a03391 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jun 2026 15:38:39 +0300 Subject: [PATCH 06/16] fix perms --- src/Database/Adapter/MariaDB.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 0727b3aac..5d4c1bd4b 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1005,7 +1005,8 @@ public function updateDocument(Document $collection, string $id, Document $docum * Get current permissions from the database */ $sqlPermissions = $this->getPDO()->prepare($sql); - $sqlPermissions->bindValue(':_uid', $document->getId()); + + $sqlPermissions->bindValue(':_uid', $id); if ($this->sharedTables) { $sqlPermissions->bindValue(':_tenant', $this->tenant); @@ -1078,7 +1079,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $removeQuery = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $removeQuery); $stmtRemovePermissions = $this->getPDO()->prepare($removeQuery); - $stmtRemovePermissions->bindValue(':_uid', $document->getId()); + $stmtRemovePermissions->bindValue(':_uid', $id); if ($this->sharedTables) { $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); @@ -1126,7 +1127,8 @@ public function updateDocument(Document $collection, string $id, Document $docum $stmtAddPermissions = $this->getPDO()->prepare($sql); - $stmtAddPermissions->bindValue(":_uid", $document->getId()); + $newUid = $document->offsetExists('$id') ? $document->getId() : $id; + $stmtAddPermissions->bindValue(":_uid", $newUid); if ($this->sharedTables) { $stmtAddPermissions->bindValue(":_tenant", $this->tenant); From e29ee2499f98a64b4ca34442855e1f348fe9f000 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jun 2026 15:46:13 +0300 Subject: [PATCH 07/16] fix createdAt --- src/Database/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 19d57bd1c..e20cd6e3f 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6159,7 +6159,7 @@ public function updateDocument(string $collection, string $id, Document $documen $skipPermissionsUpdate = ($originalPermissions === $currentPermissions); } - if (!$this->preserveDates) { + if (!$this->preserveDates || $document->getCreatedAt() === null) { $document->removeAttribute('$createdAt'); } From f94cc01890e9fe6228aa06142b5ba0e180303704 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:54:11 +0000 Subject: [PATCH 08/16] =?UTF-8?q?(fix):=20CI=20=E2=80=94=20propagate=20par?= =?UTF-8?q?tial-update=20semantics=20to=20SQLite/Postgres=20adapters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The partial-update change in Database.php only sets internal attributes (`$createdAt`, `$updatedAt`, `$permissions`, `$id`) on the Document when the user explicitly provided them. MariaDB's updateDocument was updated to honor that (offsetExists checks before writing `_createdAt`, `_uid`, `_permissions`), but SQLite and Postgres still unconditionally wrote those columns — using `$document->getCreatedAt()`/`getPermissions()` (returning null/`"[]"`) and `_uid = :_newUid` bound to `$document->getId()` (empty string for partial updates without `$id`). SQLite especially blew up because the row's `_uid` got rewritten to an empty string, so subsequent `getDocument` calls returned null and any operator-driven update appeared to lose its result. Also makes `_updatedAt` conditional in MariaDB so partial updates with `shouldUpdate=false` (e.g. relationship-only updates) don't overwrite the parent row's `_updatedAt` with NULL. Co-Authored-By: Claude Opus 4.7 --- src/Database/Adapter/MariaDB.php | 4 +++- src/Database/Adapter/Postgres.php | 25 +++++++++++++++++-------- src/Database/Adapter/SQLite.php | 25 +++++++++++++++++-------- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 5d4c1bd4b..d222080ce 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -977,7 +977,9 @@ public function updateDocument(Document $collection, string $id, Document $docum $spatialAttributes = $this->getSpatialAttributes($collection); $collection = $collection->getId(); $attributes = $document->getAttributes(); - $attributes['_updatedAt'] = $document->getUpdatedAt(); + if ($document->offsetExists('$updatedAt')) { + $attributes['_updatedAt'] = $document->getUpdatedAt(); + } if ($document->offsetExists('$id')) { $attributes['_uid'] = $document->getId(); } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index d81cdec0b..ee180e2ca 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1136,9 +1136,18 @@ public function updateDocument(Document $collection, string $id, Document $docum $spatialAttributes = $this->getSpatialAttributes($collection); $collection = $collection->getId(); $attributes = $document->getAttributes(); - $attributes['_createdAt'] = $document->getCreatedAt(); - $attributes['_updatedAt'] = $document->getUpdatedAt(); - $attributes['_permissions'] = json_encode($document->getPermissions()); + if ($document->offsetExists('$updatedAt')) { + $attributes['_updatedAt'] = $document->getUpdatedAt(); + } + if ($document->offsetExists('$id')) { + $attributes['_uid'] = $document->getId(); + } + if ($document->offsetExists('$createdAt')) { + $attributes['_createdAt'] = $document->getCreatedAt(); + } + if ($document->offsetExists('$permissions')) { + $attributes['_permissions'] = json_encode($document->getPermissions()); + } $name = $this->filter($collection); $columns = ''; @@ -1157,7 +1166,7 @@ public function updateDocument(Document $collection, string $id, Document $docum * Get current permissions from the database */ $permissionsStmt = $this->getPDO()->prepare($sql); - $permissionsStmt->bindValue(':_uid', $document->getId()); + $permissionsStmt->bindValue(':_uid', $id); if ($this->sharedTables) { $permissionsStmt->bindValue(':_tenant', $this->tenant); @@ -1230,7 +1239,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $removeQuery = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $removeQuery); $stmtRemovePermissions = $this->getPDO()->prepare($removeQuery); - $stmtRemovePermissions->bindValue(':_uid', $document->getId()); + $stmtRemovePermissions->bindValue(':_uid', $id); if ($this->sharedTables) { $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); @@ -1264,7 +1273,8 @@ public function updateDocument(Document $collection, string $id, Document $docum $sql = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $sql); $stmtAddPermissions = $this->getPDO()->prepare($sql); - $stmtAddPermissions->bindValue(":_uid", $document->getId()); + $newUid = $document->offsetExists('$id') ? $document->getId() : $id; + $stmtAddPermissions->bindValue(":_uid", $newUid); if ($this->sharedTables) { $stmtAddPermissions->bindValue(':_tenant', $this->tenant); } @@ -1312,7 +1322,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $sql = " UPDATE {$this->getSQLTable($name)} - SET {$columns} _uid = :_newUid + SET " . \rtrim($columns, ',') . " WHERE _id=:_sequence {$this->getTenantQuery($collection)} "; @@ -1322,7 +1332,6 @@ public function updateDocument(Document $collection, string $id, Document $docum $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_sequence', $document->getSequence()); - $stmt->bindValue(':_newUid', $document->getId()); if ($this->sharedTables) { $stmt->bindValue(':_tenant', $this->tenant); diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 5b07ce3b7..21ab4c698 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -1272,9 +1272,18 @@ public function updateDocument(Document $collection, string $id, Document $docum $spatialAttributes = $this->getSpatialAttributes($collection); $collection = $collection->getId(); $attributes = $document->getAttributes(); - $attributes['_createdAt'] = $document->getCreatedAt(); - $attributes['_updatedAt'] = $document->getUpdatedAt(); - $attributes['_permissions'] = json_encode($document->getPermissions()); + if ($document->offsetExists('$updatedAt')) { + $attributes['_updatedAt'] = $document->getUpdatedAt(); + } + if ($document->offsetExists('$id')) { + $attributes['_uid'] = $document->getId(); + } + if ($document->offsetExists('$createdAt')) { + $attributes['_createdAt'] = $document->getCreatedAt(); + } + if ($document->offsetExists('$permissions')) { + $attributes['_permissions'] = json_encode($document->getPermissions()); + } if ($this->sharedTables) { $attributes['_tenant'] = $this->tenant; @@ -1297,7 +1306,7 @@ public function updateDocument(Document $collection, string $id, Document $docum * Get current permissions from the database */ $permissionsStmt = $this->getPDO()->prepare($sql); - $permissionsStmt->bindValue(':_uid', $document->getId()); + $permissionsStmt->bindValue(':_uid', $id); if ($this->sharedTables) { $permissionsStmt->bindValue(':_tenant', $this->tenant); @@ -1369,7 +1378,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $removeQuery = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $removeQuery); $stmtRemovePermissions = $this->getPDO()->prepare($removeQuery); - $stmtRemovePermissions->bindValue(':_uid', $document->getId()); + $stmtRemovePermissions->bindValue(':_uid', $id); if ($this->sharedTables) { $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); @@ -1404,7 +1413,8 @@ public function updateDocument(Document $collection, string $id, Document $docum $stmtAddPermissions = $this->getPDO()->prepare($sql); - $stmtAddPermissions->bindValue(":_uid", $document->getId()); + $newUid = $document->offsetExists('$id') ? $document->getId() : $id; + $stmtAddPermissions->bindValue(":_uid", $newUid); if ($this->sharedTables) { $stmtAddPermissions->bindValue(":_tenant", $this->tenant); } @@ -1456,7 +1466,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $sql = " UPDATE `{$this->getNamespace()}_{$name}` - SET {$columns}, _uid = :_newUid + SET {$columns} WHERE _uid = :_existingUid {$this->getTenantQuery($collection)} "; @@ -1466,7 +1476,6 @@ public function updateDocument(Document $collection, string $id, Document $docum $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_existingUid', $id); - $stmt->bindValue(':_newUid', $document->getId()); if ($this->sharedTables) { $stmt->bindValue(':_tenant', $this->tenant); From 3a0c449d8884d9f27d46c431c3aafbbe3d24a02a Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jun 2026 16:30:14 +0300 Subject: [PATCH 09/16] use clone --- src/Database/Database.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index e20cd6e3f..e20321cee 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6136,6 +6136,8 @@ public function updateDocument(string $collection, string $id, Document $documen throw new DatabaseException('Must define $id attribute'); } + $document = clone $document; + $collection = $this->silent(fn () => $this->getCollection($collection)); $newUpdatedAt = $document->getUpdatedAt(); @@ -6307,7 +6309,7 @@ public function updateDocument(string $collection, string $id, Document $documen throw new ConflictException('Document was updated after the request timestamp'); } - $document = $this->encode($collection, $document); + $document = $this->encode($collection, $document, applyDefaults: false); if ($this->validate) { $structureValidator = new PartialStructure( From cec6e4395112f48778bbd41fc6d1b3ac1d96eb52 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jun 2026 17:01:05 +0300 Subject: [PATCH 10/16] fix perms --- src/Database/Database.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Database/Database.php b/src/Database/Database.php index e20321cee..0c067893d 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6700,6 +6700,11 @@ private function updateDocumentRelationships(Document $collection, Document $old try { switch ($relationType) { case Database::RELATION_ONE_TO_ONE: + if (\is_array($value)) { + $value = new Document($value); + $document->setAttribute($key, $value); + } + if (!$twoWay) { if ($side === Database::RELATION_SIDE_CHILD) { throw new RelationshipException('Invalid relationship value. Cannot set a value from the child side of a oneToOne relationship when twoWay is false.'); @@ -6824,6 +6829,9 @@ private function updateDocumentRelationships(Document $collection, Document $old throw new RelationshipException('Invalid relationship value. Must be either an array of documents or document IDs, ' . \gettype($value) . ' given.'); } + $value = \array_map(fn ($item) => \is_array($item) ? new Document($item) : $item, $value); + $document->setAttribute($key, $value); + $oldIds = \array_map(fn ($document) => $document->getId(), $oldValue); $newIds = \array_map(function ($item) { From 76b2374f74744710c4aaaefb0087c3d2b233a016 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jun 2026 17:21:35 +0300 Subject: [PATCH 11/16] run --- src/Database/Database.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 0c067893d..c310e280e 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6700,7 +6700,7 @@ private function updateDocumentRelationships(Document $collection, Document $old try { switch ($relationType) { case Database::RELATION_ONE_TO_ONE: - if (\is_array($value)) { + if (\is_array($value) && !\array_is_list($value)) { $value = new Document($value); $document->setAttribute($key, $value); } @@ -6898,6 +6898,11 @@ private function updateDocumentRelationships(Document $collection, Document $old break; } + if (\is_array($value) && !\array_is_list($value)) { + $value = new Document($value); + $document->setAttribute($key, $value); + } + if (\is_string($value)) { $related = $this->skipRelationships( fn () => $this->getDocument($relatedCollection->getId(), $value, [Query::select(['$id'])]) From d14540d10edf764d675480788333720620c0192d Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jun 2026 17:35:16 +0300 Subject: [PATCH 12/16] revert --- src/Database/Adapter/Postgres.php | 51 +++++++++++++------------------ src/Database/Adapter/SQLite.php | 39 +++++++++-------------- 2 files changed, 36 insertions(+), 54 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index ee180e2ca..090dbc87c 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1136,18 +1136,9 @@ public function updateDocument(Document $collection, string $id, Document $docum $spatialAttributes = $this->getSpatialAttributes($collection); $collection = $collection->getId(); $attributes = $document->getAttributes(); - if ($document->offsetExists('$updatedAt')) { - $attributes['_updatedAt'] = $document->getUpdatedAt(); - } - if ($document->offsetExists('$id')) { - $attributes['_uid'] = $document->getId(); - } - if ($document->offsetExists('$createdAt')) { - $attributes['_createdAt'] = $document->getCreatedAt(); - } - if ($document->offsetExists('$permissions')) { - $attributes['_permissions'] = json_encode($document->getPermissions()); - } + $attributes['_createdAt'] = $document->getCreatedAt(); + $attributes['_updatedAt'] = $document->getUpdatedAt(); + $attributes['_permissions'] = json_encode($document->getPermissions()); $name = $this->filter($collection); $columns = ''; @@ -1166,7 +1157,7 @@ public function updateDocument(Document $collection, string $id, Document $docum * Get current permissions from the database */ $permissionsStmt = $this->getPDO()->prepare($sql); - $permissionsStmt->bindValue(':_uid', $id); + $permissionsStmt->bindValue(':_uid', $document->getId()); if ($this->sharedTables) { $permissionsStmt->bindValue(':_tenant', $this->tenant); @@ -1239,7 +1230,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $removeQuery = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $removeQuery); $stmtRemovePermissions = $this->getPDO()->prepare($removeQuery); - $stmtRemovePermissions->bindValue(':_uid', $id); + $stmtRemovePermissions->bindValue(':_uid', $document->getId()); if ($this->sharedTables) { $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); @@ -1273,8 +1264,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $sql = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $sql); $stmtAddPermissions = $this->getPDO()->prepare($sql); - $newUid = $document->offsetExists('$id') ? $document->getId() : $id; - $stmtAddPermissions->bindValue(":_uid", $newUid); + $stmtAddPermissions->bindValue(":_uid", $document->getId()); if ($this->sharedTables) { $stmtAddPermissions->bindValue(':_tenant', $this->tenant); } @@ -1322,7 +1312,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $sql = " UPDATE {$this->getSQLTable($name)} - SET " . \rtrim($columns, ',') . " + SET {$columns} _uid = :_newUid WHERE _id=:_sequence {$this->getTenantQuery($collection)} "; @@ -1332,6 +1322,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_sequence', $document->getSequence()); + $stmt->bindValue(':_newUid', $document->getId()); if ($this->sharedTables) { $stmt->bindValue(':_tenant', $this->tenant); @@ -1596,7 +1587,7 @@ public function getConnectionId(): string * @param string $alias * @param string $placeholder * @return string - */ + */ protected function handleDistanceSpatialQueries(Query $query, array &$binds, string $attribute, string $alias, string $placeholder): string { $distanceParams = $query->getValues()[0]; @@ -1841,7 +1832,7 @@ protected function getSQLCondition(Query $query, array &$binds, ?string $forColl $binds[":{$placeholder}_0"] = \json_encode($query->getValues()); return "{$alias}.{$attribute} @> :{$placeholder}_0::jsonb"; } - // no break + // no break case Query::TYPE_CONTAINS: case Query::TYPE_CONTAINS_ANY: case Query::TYPE_NOT_CONTAINS: @@ -1849,7 +1840,7 @@ protected function getSQLCondition(Query $query, array &$binds, ?string $forColl $operator = '@>'; } - // no break + // no break default: $conditions = []; $operator = $operator ?? $this->getSQLOperator($query->getMethod()); @@ -2063,7 +2054,7 @@ protected function getRandomOrder(): string * Size of POINT spatial type * * @return int - */ + */ protected function getMaxPointSize(): int { // https://stackoverflow.com/questions/30455025/size-of-data-type-geographypoint-4326-in-postgis @@ -2281,7 +2272,7 @@ protected function quote(string $string): string * Is spatial attributes supported? * * @return bool - */ + */ public function getSupportForSpatialAttributes(): bool { return true; @@ -2291,7 +2282,7 @@ public function getSupportForSpatialAttributes(): bool * Are object (JSONB) attributes supported? * * @return bool - */ + */ public function getSupportForObject(): bool { return true; @@ -2311,7 +2302,7 @@ public function getSupportForObjectIndexes(): bool * Does the adapter support null values in spatial indexes? * * @return bool - */ + */ public function getSupportForSpatialIndexNull(): bool { return true; @@ -2321,7 +2312,7 @@ public function getSupportForSpatialIndexNull(): bool * Does the adapter includes boundary during spatial contains? * * @return bool - */ + */ public function getSupportForBoundaryInclusiveContains(): bool { return true; @@ -2331,7 +2322,7 @@ public function getSupportForBoundaryInclusiveContains(): bool * Does the adapter support order attribute in spatial indexes? * * @return bool - */ + */ public function getSupportForSpatialIndexOrder(): bool { return false; @@ -2727,7 +2718,7 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind } return "{$quotedColumn} = POWER(COALESCE({$columnRef}, 0), :$bindKey)"; - // String operators + // String operators case Operator::TYPE_STRING_CONCAT: $bindKey = "op_{$bindIndex}"; $bindIndex++; @@ -2740,11 +2731,11 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind $bindIndex++; return "{$quotedColumn} = REPLACE(COALESCE({$columnRef}, ''), :$searchKey, :$replaceKey)"; - // Boolean operators + // Boolean operators case Operator::TYPE_TOGGLE: return "{$quotedColumn} = NOT COALESCE({$columnRef}, FALSE)"; - // Array operators + // Array operators case Operator::TYPE_ARRAY_APPEND: $bindKey = "op_{$bindIndex}"; $bindIndex++; @@ -2829,7 +2820,7 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind END ), '[]'::jsonb)"; - // Date operators + // Date operators case Operator::TYPE_DATE_ADD_DAYS: $bindKey = "op_{$bindIndex}"; $bindIndex++; diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 21ab4c698..596681a5a 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -1272,18 +1272,9 @@ public function updateDocument(Document $collection, string $id, Document $docum $spatialAttributes = $this->getSpatialAttributes($collection); $collection = $collection->getId(); $attributes = $document->getAttributes(); - if ($document->offsetExists('$updatedAt')) { - $attributes['_updatedAt'] = $document->getUpdatedAt(); - } - if ($document->offsetExists('$id')) { - $attributes['_uid'] = $document->getId(); - } - if ($document->offsetExists('$createdAt')) { - $attributes['_createdAt'] = $document->getCreatedAt(); - } - if ($document->offsetExists('$permissions')) { - $attributes['_permissions'] = json_encode($document->getPermissions()); - } + $attributes['_createdAt'] = $document->getCreatedAt(); + $attributes['_updatedAt'] = $document->getUpdatedAt(); + $attributes['_permissions'] = json_encode($document->getPermissions()); if ($this->sharedTables) { $attributes['_tenant'] = $this->tenant; @@ -1306,7 +1297,7 @@ public function updateDocument(Document $collection, string $id, Document $docum * Get current permissions from the database */ $permissionsStmt = $this->getPDO()->prepare($sql); - $permissionsStmt->bindValue(':_uid', $id); + $permissionsStmt->bindValue(':_uid', $document->getId()); if ($this->sharedTables) { $permissionsStmt->bindValue(':_tenant', $this->tenant); @@ -1378,7 +1369,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $removeQuery = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $removeQuery); $stmtRemovePermissions = $this->getPDO()->prepare($removeQuery); - $stmtRemovePermissions->bindValue(':_uid', $id); + $stmtRemovePermissions->bindValue(':_uid', $document->getId()); if ($this->sharedTables) { $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); @@ -1413,8 +1404,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $stmtAddPermissions = $this->getPDO()->prepare($sql); - $newUid = $document->offsetExists('$id') ? $document->getId() : $id; - $stmtAddPermissions->bindValue(":_uid", $newUid); + $stmtAddPermissions->bindValue(":_uid", $document->getId()); if ($this->sharedTables) { $stmtAddPermissions->bindValue(":_tenant", $this->tenant); } @@ -1466,7 +1456,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $sql = " UPDATE `{$this->getNamespace()}_{$name}` - SET {$columns} + SET {$columns}, _uid = :_newUid WHERE _uid = :_existingUid {$this->getTenantQuery($collection)} "; @@ -1476,6 +1466,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_existingUid', $id); + $stmt->bindValue(':_newUid', $document->getId()); if ($this->sharedTables) { $stmt->bindValue(':_tenant', $this->tenant); @@ -2256,7 +2247,7 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind } return "{$quotedColumn} = POWER(COALESCE({$quotedColumn}, 0), :$bindKey)"; - // String operators + // String operators case Operator::TYPE_STRING_CONCAT: $bindKey = "op_{$bindIndex}"; $bindIndex++; @@ -2269,12 +2260,12 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind $bindIndex++; return "{$quotedColumn} = REPLACE({$quotedColumn}, :$searchKey, :$replaceKey)"; - // Boolean operators + // Boolean operators case Operator::TYPE_TOGGLE: // SQLite: toggle boolean (0 or 1), treat NULL as 0 return "{$quotedColumn} = CASE WHEN COALESCE({$quotedColumn}, 0) = 0 THEN 1 ELSE 0 END"; - // Array operators + // Array operators case Operator::TYPE_ARRAY_APPEND: $bindKey = "op_{$bindIndex}"; $bindIndex++; @@ -2438,13 +2429,13 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind )"; } - // no break + // no break default: return "{$quotedColumn} = {$quotedColumn}"; } - // Date operators - // no break + // Date operators + // no break case Operator::TYPE_DATE_ADD_DAYS: $bindKey = "op_{$bindIndex}"; $bindIndex++; @@ -2947,7 +2938,7 @@ public function getSchemaIndexes(string $collection): array * columns: array, * lengths: array, * }> - */ + */ protected function getFulltextSchemaIndexes(string $collection): array { $tables = $this->findFulltextTables($collection); From 40d4b5ccf89281116a4938ad8c5366106a4e91ec Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jun 2026 17:55:32 +0300 Subject: [PATCH 13/16] revert --- src/Database/Database.php | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index c310e280e..a0c0bd321 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6136,8 +6136,6 @@ public function updateDocument(string $collection, string $id, Document $documen throw new DatabaseException('Must define $id attribute'); } - $document = clone $document; - $collection = $this->silent(fn () => $this->getCollection($collection)); $newUpdatedAt = $document->getUpdatedAt(); @@ -6321,7 +6319,7 @@ public function updateDocument(string $collection, string $id, Document $documen supportUnsignedBigInt: $this->adapter->getSupportForUnsignedBigInt(), currentDocument: $old ); - if (!$structureValidator->isValid($document)) { + if (!$structureValidator->isValid($document)) { // Make sure updated structure still apply collection rules (if any) throw new StructureException($structureValidator->getDescription()); } } @@ -6700,11 +6698,6 @@ private function updateDocumentRelationships(Document $collection, Document $old try { switch ($relationType) { case Database::RELATION_ONE_TO_ONE: - if (\is_array($value) && !\array_is_list($value)) { - $value = new Document($value); - $document->setAttribute($key, $value); - } - if (!$twoWay) { if ($side === Database::RELATION_SIDE_CHILD) { throw new RelationshipException('Invalid relationship value. Cannot set a value from the child side of a oneToOne relationship when twoWay is false.'); @@ -6829,9 +6822,6 @@ private function updateDocumentRelationships(Document $collection, Document $old throw new RelationshipException('Invalid relationship value. Must be either an array of documents or document IDs, ' . \gettype($value) . ' given.'); } - $value = \array_map(fn ($item) => \is_array($item) ? new Document($item) : $item, $value); - $document->setAttribute($key, $value); - $oldIds = \array_map(fn ($document) => $document->getId(), $oldValue); $newIds = \array_map(function ($item) { @@ -6898,11 +6888,6 @@ private function updateDocumentRelationships(Document $collection, Document $old break; } - if (\is_array($value) && !\array_is_list($value)) { - $value = new Document($value); - $document->setAttribute($key, $value); - } - if (\is_string($value)) { $related = $this->skipRelationships( fn () => $this->getDocument($relatedCollection->getId(), $value, [Query::select(['$id'])]) From 93e0eef904aa13f9c419c709f404afc7202e7c89 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jun 2026 17:56:55 +0300 Subject: [PATCH 14/16] remove clone --- src/Database/Database.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index a0c0bd321..1cad6f5e8 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6138,7 +6138,6 @@ public function updateDocument(string $collection, string $id, Document $documen $collection = $this->silent(fn () => $this->getCollection($collection)); $newUpdatedAt = $document->getUpdatedAt(); - $document = $this->withTransaction(function () use ($collection, $id, $document, $newUpdatedAt) { $time = DateTime::now(); $old = $this->authorization->skip(fn () => $this->silent( From 5f0caca449bb35ed90dd71df40488e81a260aba4 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jun 2026 17:59:26 +0300 Subject: [PATCH 15/16] Pstgres --- src/Database/Adapter/Postgres.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 090dbc87c..d81cdec0b 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1587,7 +1587,7 @@ public function getConnectionId(): string * @param string $alias * @param string $placeholder * @return string - */ + */ protected function handleDistanceSpatialQueries(Query $query, array &$binds, string $attribute, string $alias, string $placeholder): string { $distanceParams = $query->getValues()[0]; @@ -1832,7 +1832,7 @@ protected function getSQLCondition(Query $query, array &$binds, ?string $forColl $binds[":{$placeholder}_0"] = \json_encode($query->getValues()); return "{$alias}.{$attribute} @> :{$placeholder}_0::jsonb"; } - // no break + // no break case Query::TYPE_CONTAINS: case Query::TYPE_CONTAINS_ANY: case Query::TYPE_NOT_CONTAINS: @@ -1840,7 +1840,7 @@ protected function getSQLCondition(Query $query, array &$binds, ?string $forColl $operator = '@>'; } - // no break + // no break default: $conditions = []; $operator = $operator ?? $this->getSQLOperator($query->getMethod()); @@ -2054,7 +2054,7 @@ protected function getRandomOrder(): string * Size of POINT spatial type * * @return int - */ + */ protected function getMaxPointSize(): int { // https://stackoverflow.com/questions/30455025/size-of-data-type-geographypoint-4326-in-postgis @@ -2272,7 +2272,7 @@ protected function quote(string $string): string * Is spatial attributes supported? * * @return bool - */ + */ public function getSupportForSpatialAttributes(): bool { return true; @@ -2282,7 +2282,7 @@ public function getSupportForSpatialAttributes(): bool * Are object (JSONB) attributes supported? * * @return bool - */ + */ public function getSupportForObject(): bool { return true; @@ -2302,7 +2302,7 @@ public function getSupportForObjectIndexes(): bool * Does the adapter support null values in spatial indexes? * * @return bool - */ + */ public function getSupportForSpatialIndexNull(): bool { return true; @@ -2312,7 +2312,7 @@ public function getSupportForSpatialIndexNull(): bool * Does the adapter includes boundary during spatial contains? * * @return bool - */ + */ public function getSupportForBoundaryInclusiveContains(): bool { return true; @@ -2322,7 +2322,7 @@ public function getSupportForBoundaryInclusiveContains(): bool * Does the adapter support order attribute in spatial indexes? * * @return bool - */ + */ public function getSupportForSpatialIndexOrder(): bool { return false; @@ -2718,7 +2718,7 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind } return "{$quotedColumn} = POWER(COALESCE({$columnRef}, 0), :$bindKey)"; - // String operators + // String operators case Operator::TYPE_STRING_CONCAT: $bindKey = "op_{$bindIndex}"; $bindIndex++; @@ -2731,11 +2731,11 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind $bindIndex++; return "{$quotedColumn} = REPLACE(COALESCE({$columnRef}, ''), :$searchKey, :$replaceKey)"; - // Boolean operators + // Boolean operators case Operator::TYPE_TOGGLE: return "{$quotedColumn} = NOT COALESCE({$columnRef}, FALSE)"; - // Array operators + // Array operators case Operator::TYPE_ARRAY_APPEND: $bindKey = "op_{$bindIndex}"; $bindIndex++; @@ -2820,7 +2820,7 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind END ), '[]'::jsonb)"; - // Date operators + // Date operators case Operator::TYPE_DATE_ADD_DAYS: $bindKey = "op_{$bindIndex}"; $bindIndex++; From 5cda7374cd30a378683cebed5e5e0944aab1edcb Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 10 Jun 2026 18:26:40 +0300 Subject: [PATCH 16/16] Pstgres --- src/Database/Adapter/SQLite.php | 14 +++++++------- src/Database/Database.php | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 596681a5a..5b07ce3b7 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -2247,7 +2247,7 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind } return "{$quotedColumn} = POWER(COALESCE({$quotedColumn}, 0), :$bindKey)"; - // String operators + // String operators case Operator::TYPE_STRING_CONCAT: $bindKey = "op_{$bindIndex}"; $bindIndex++; @@ -2260,12 +2260,12 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind $bindIndex++; return "{$quotedColumn} = REPLACE({$quotedColumn}, :$searchKey, :$replaceKey)"; - // Boolean operators + // Boolean operators case Operator::TYPE_TOGGLE: // SQLite: toggle boolean (0 or 1), treat NULL as 0 return "{$quotedColumn} = CASE WHEN COALESCE({$quotedColumn}, 0) = 0 THEN 1 ELSE 0 END"; - // Array operators + // Array operators case Operator::TYPE_ARRAY_APPEND: $bindKey = "op_{$bindIndex}"; $bindIndex++; @@ -2429,13 +2429,13 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind )"; } - // no break + // no break default: return "{$quotedColumn} = {$quotedColumn}"; } - // Date operators - // no break + // Date operators + // no break case Operator::TYPE_DATE_ADD_DAYS: $bindKey = "op_{$bindIndex}"; $bindIndex++; @@ -2938,7 +2938,7 @@ public function getSchemaIndexes(string $collection): array * columns: array, * lengths: array, * }> - */ + */ protected function getFulltextSchemaIndexes(string $collection): array { $tables = $this->findFulltextTables($collection); diff --git a/src/Database/Database.php b/src/Database/Database.php index 1cad6f5e8..54e5f8231 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6306,7 +6306,7 @@ public function updateDocument(string $collection, string $id, Document $documen throw new ConflictException('Document was updated after the request timestamp'); } - $document = $this->encode($collection, $document, applyDefaults: false); + $document = $this->encode($collection, clone $document, applyDefaults: false); if ($this->validate) { $structureValidator = new PartialStructure(