From d63dcbd67b7f605dcdf3a3fd2afa1265a7251b87 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 31 Jul 2025 00:40:28 +0530 Subject: [PATCH 1/8] allow modification of created at and updated at in update document and update documents --- src/Database/Database.php | 23 +- src/Database/Validator/PartialStructure.php | 12 +- tests/e2e/Adapter/Scopes/DocumentTests.php | 285 +++++++++++++++++--- tests/e2e/Adapter/Scopes/GeneralTests.php | 31 +++ 4 files changed, 305 insertions(+), 46 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 2eefa9340..e1dfc39e2 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4119,10 +4119,11 @@ public function updateDocument(string $collection, string $id, Document $documen $skipPermissionsUpdate = ($originalPermissions === $currentPermissions); } + $createdAt = $document->getCreatedAt(); $document = \array_merge($old->getArrayCopy(), $document->getArrayCopy()); $document['$collection'] = $old->getAttribute('$collection'); // Make sure user doesn't switch collection ID - $document['$createdAt'] = $old->getCreatedAt(); // Make sure user doesn't switch createdAt + $document['$createdAt'] = ($createdAt === null || !$this->preserveDates) ? $old->getCreatedAt() : $createdAt; // Make sure user doesn't switch createdAt if ($this->adapter->getSharedTables()) { $document['$tenant'] = $old->getTenant(); // Make sure user doesn't switch tenant @@ -4251,7 +4252,7 @@ public function updateDocument(string $collection, string $id, Document $documen if ($shouldUpdate) { $updatedAt = $document->getUpdatedAt(); - $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); + $document->setAttribute('$updatedAt', ($updatedAt === null || !$this->preserveDates) ? $time : $updatedAt); } // Check if document was updated after the request timestamp @@ -4365,21 +4366,23 @@ public function updateDocuments( if (!empty($cursor) && $cursor->getCollection() !== $collection->getId()) { throw new DatabaseException("Cursor document must be from the same Collection."); } - + $attributesToCheckForRequiredValidation = ['$updatedAt']; unset($updates['$id']); - unset($updates['$createdAt']); unset($updates['$tenant']); - + if (($updates->getCreatedAt() === null || !$this->preserveDates)) { + unset($updates['$createdAt']); + } else { + $updates['$createdAt'] = $updates->getCreatedAt(); + $attributesToCheckForRequiredValidation[] = '$createdAt'; + } if ($this->adapter->getSharedTables()) { $updates['$tenant'] = $this->adapter->getTenant(); } - if (!$this->preserveDates) { - $updates['$updatedAt'] = DateTime::now(); - } + $updatedAt = $updates->getUpdatedAt(); + $updates['$updatedAt'] = ($updatedAt === null || !$this->preserveDates) ? DateTime::now() : $updatedAt; $updates = $this->encode($collection, $updates); - // Check new document structure $validator = new PartialStructure( $collection, @@ -4387,7 +4390,7 @@ public function updateDocuments( $this->adapter->getMaxDateTime(), ); - if (!$validator->isValid($updates)) { + if (!$validator->isValid($updates, $attributesToCheckForRequiredValidation)) { throw new StructureException($validator->getDescription()); } diff --git a/src/Database/Validator/PartialStructure.php b/src/Database/Validator/PartialStructure.php index 1f52a451c..2fdff6c6e 100644 --- a/src/Database/Validator/PartialStructure.php +++ b/src/Database/Validator/PartialStructure.php @@ -13,10 +13,11 @@ class PartialStructure extends Structure * Returns true if valid or false if not. * * @param mixed $document + * @param array $requiredAttributes optional list of required attributes to check * * @return bool */ - public function isValid($document): bool + public function isValid($document, array $requiredAttributes = []): bool { if (!$document instanceof Document) { $this->message = 'Value must be an instance of Document'; @@ -36,7 +37,16 @@ public function isValid($document): bool $name = $attribute['$id'] ?? ''; $keys[$name] = $attribute; } + $requiredAttributesMap = []; + foreach ($this->attributes as $attribute) { + if ($attribute['required'] === true && in_array($attribute['$id'], $requiredAttributes)) { + $requiredAttributesMap[] = $attribute; + } + } + if (!$this->checkForAllRequiredValues($structure, $requiredAttributesMap, $keys)) { + return false; + } if (!$this->checkForUnknownAttributes($structure, $keys)) { return false; } diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 201ec9d67..e611c794d 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -4635,7 +4635,7 @@ public function testEmptyOperatorValues(): void } } - public function testModifyDocumentWithDates(): void + public function testDateTimeDocument(): void { /** * @var Database $database @@ -4684,72 +4684,287 @@ public function testModifyDocumentWithDates(): void $database->deleteCollection($collection); } - public function testModifyBulkDocumentWithDates(): void + public function testNormalDocumentDateOperations(): void { /** @var Database $database */ $database = static::getDatabase(); - - $collection = 'bulk_modify_dates'; - $date = '2000-01-01T10:00:00.000+00:00'; + $collection = 'normal_date_operations'; + $database->createCollection($collection); + $this->assertEquals(true, $database->createAttribute($collection, 'string', Database::VAR_STRING, 128, false)); $database->setPreserveDates(true); + + $createDate = '2000-01-01T10:00:00.000+00:00'; + $updateDate = '2000-02-01T15:30:00.000+00:00'; + $date1 = '2000-01-01T10:00:00.000+00:00'; + $date2 = '2000-02-01T15:30:00.000+00:00'; + $date3 = '2000-03-01T20:45:00.000+00:00'; + // Test 1: Create with custom createdAt, then update with custom updatedAt + $doc = $database->createDocument($collection, new Document([ + '$id' => 'doc1', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'initial', + '$createdAt' => $createDate + ])); + + $this->assertEquals($createDate, $doc->getAttribute('$createdAt')); + $this->assertNotEquals($createDate, $doc->getAttribute('$updatedAt')); + + // Update with custom updatedAt + $doc->setAttribute('string', 'updated'); + $doc->setAttribute('$updatedAt', $updateDate); + $updatedDoc = $database->updateDocument($collection, 'doc1', $doc); + + $this->assertEquals($createDate, $updatedDoc->getAttribute('$createdAt')); + $this->assertEquals($updateDate, $updatedDoc->getAttribute('$updatedAt')); + + // Test 2: Create with both custom dates + $doc2 = $database->createDocument($collection, new Document([ + '$id' => 'doc2', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'both_dates', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ])); + + $this->assertEquals($createDate, $doc2->getAttribute('$createdAt')); + $this->assertEquals($updateDate, $doc2->getAttribute('$updatedAt')); + + // Test 3: Create without dates, then update with custom dates + $doc3 = $database->createDocument($collection, new Document([ + '$id' => 'doc3', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'no_dates' + ])); + + + $doc3->setAttribute('string', 'updated_no_dates'); + $doc3->setAttribute('$createdAt', $createDate); + $doc3->setAttribute('$updatedAt', $updateDate); + $updatedDoc3 = $database->updateDocument($collection, 'doc3', $doc3); + + $this->assertEquals($createDate, $updatedDoc3->getAttribute('$createdAt')); + $this->assertEquals($updateDate, $updatedDoc3->getAttribute('$updatedAt')); + + // Test 4: Update only createdAt + $doc4 = $database->createDocument($collection, new Document([ + '$id' => 'doc4', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'initial' + ])); + + $originalCreatedAt4 = $doc4->getAttribute('$createdAt'); + $originalUpdatedAt4 = $doc4->getAttribute('$updatedAt'); + + $doc4->setAttribute('$updatedAt', null); + $doc4->setAttribute('$createdAt', null); + $updatedDoc4 = $database->updateDocument($collection, 'doc4', document: $doc4); + + $this->assertEquals($originalCreatedAt4, $updatedDoc4->getAttribute('$createdAt')); + $this->assertNotEquals($originalUpdatedAt4, $updatedDoc4->getAttribute('$updatedAt')); + + // Test 5: Update only updatedAt + $updatedDoc4->setAttribute('$updatedAt', $updateDate); + $updatedDoc4->setAttribute('$createdAt', $createDate); + $finalDoc4 = $database->updateDocument($collection, 'doc4', $updatedDoc4); + + $this->assertEquals($createDate, $finalDoc4->getAttribute('$createdAt')); + $this->assertEquals($updateDate, $finalDoc4->getAttribute('$updatedAt')); + + // Test 6: Create with updatedAt, update with createdAt + $doc5 = $database->createDocument($collection, new Document([ + '$id' => 'doc5', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'doc5', + '$updatedAt' => $date2 + ])); + + $this->assertNotEquals($date2, $doc5->getAttribute('$createdAt')); + $this->assertEquals($date2, $doc5->getAttribute('$updatedAt')); + + $doc5->setAttribute('string', 'doc5_updated'); + $doc5->setAttribute('$createdAt', $date1); + $updatedDoc5 = $database->updateDocument($collection, 'doc5', $doc5); + + $this->assertEquals($date1, $updatedDoc5->getAttribute('$createdAt')); + $this->assertEquals($date2, $updatedDoc5->getAttribute('$updatedAt')); + + // Test 7: Create with both dates, update with different dates + $doc6 = $database->createDocument($collection, new Document([ + '$id' => 'doc6', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'doc6', + '$createdAt' => $date1, + '$updatedAt' => $date2 + ])); + + $this->assertEquals($date1, $doc6->getAttribute('$createdAt')); + $this->assertEquals($date2, $doc6->getAttribute('$updatedAt')); + + $doc6->setAttribute('string', 'doc6_updated'); + $doc6->setAttribute('$createdAt', $date3); + $doc6->setAttribute('$updatedAt', $date3); + $updatedDoc6 = $database->updateDocument($collection, 'doc6', $doc6); + + $this->assertEquals($date3, $updatedDoc6->getAttribute('$createdAt')); + $this->assertEquals($date3, $updatedDoc6->getAttribute('$updatedAt')); + + // Test 8: Preserve dates disabled + $database->setPreserveDates(false); + + $customDate = '2000-01-01T10:00:00.000+00:00'; + + $doc7 = $database->createDocument($collection, new Document([ + '$id' => 'doc7', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'doc7', + '$createdAt' => $customDate, + '$updatedAt' => $customDate + ])); + + $this->assertNotEquals($customDate, $doc7->getAttribute('$createdAt')); + $this->assertNotEquals($customDate, $doc7->getAttribute('$updatedAt')); + + // Update with custom dates should also be ignored + $doc7->setAttribute('string', 'updated'); + $doc7->setAttribute('$createdAt', $customDate); + $doc7->setAttribute('$updatedAt', $customDate); + $updatedDoc7 = $database->updateDocument($collection, 'doc7', $doc7); + + $this->assertNotEquals($customDate, $updatedDoc7->getAttribute('$createdAt')); + $this->assertNotEquals($customDate, $updatedDoc7->getAttribute('$updatedAt')); + + $database->setPreserveDates(false); + $database->deleteCollection($collection); + } + + public function testBulkDocumentDateOperations(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + $collection = 'bulk_date_operations'; $database->createCollection($collection); + $this->assertEquals(true, $database->createAttribute($collection, 'string', Database::VAR_STRING, 128, false)); - $this->assertTrue( - $database->createAttribute($collection, 'attr1', Database::VAR_STRING, 128, false) - ); + $database->setPreserveDates(true); + + $createDate = '2000-01-01T10:00:00.000+00:00'; + $updateDate = '2000-02-01T15:30:00.000+00:00'; + $permissions = [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())]; + // Test 1: Bulk create with different date configurations $documents = [ + new Document([ + '$id' => 'doc1', + '$permissions' => $permissions, + 'string' => 'doc1', + '$createdAt' => $createDate + ]), new Document([ '$id' => 'doc2', - '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any())], - 'attr1' => 'value2', - '$createdAt' => $date + '$permissions' => $permissions, + 'string' => 'doc2', + '$updatedAt' => $updateDate ]), new Document([ '$id' => 'doc3', - '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any())], - 'attr1' => 'value3', - '$createdAt' => $date + '$permissions' => $permissions, + 'string' => 'doc3', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate ]), new Document([ '$id' => 'doc4', - '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any())], - 'attr1' => 'value4', - '$createdAt' => null + '$permissions' => $permissions, + 'string' => 'doc4' ]), new Document([ '$id' => 'doc5', - '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any())], - 'attr1' => 'value5' + '$permissions' => $permissions, + 'string' => 'doc5', + '$createdAt' => null ]), new Document([ '$id' => 'doc6', - '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any())], - 'attr1' => 'value6', - '$createdAt' => $date, - '$updatedAt' => $date - ]), + '$permissions' => $permissions, + 'string' => 'doc6', + '$updatedAt' => null + ]) ]; - $resultDocs = $database->createDocuments($collection, $documents, batchSize: 2); + $database->createDocuments($collection, $documents); - foreach (['doc2', 'doc3', 'doc6'] as $id) { + // Verify initial state + foreach (['doc1', 'doc3'] as $id) { $doc = $database->getDocument($collection, $id); - $this->assertEquals($date, $doc->getAttribute('$createdAt'), "Mismatch for doc: $id"); - if ($id === 'doc6') { - $this->assertEquals($date, $doc->getAttribute('$updatedAt'), "updatedAt incorrectly preserved for doc: $id"); - } else { - $this->assertNotEquals($date, $doc->getAttribute('$updatedAt'), "updatedAt incorrectly preserved for doc: $id"); - } + $this->assertEquals($createDate, $doc->getAttribute('$createdAt'), "createdAt mismatch for $id"); + } + + foreach (['doc2', 'doc3'] as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertEquals($updateDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); + } + + foreach (['doc4', 'doc5', 'doc6'] as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertNotEmpty($doc->getAttribute('$createdAt'), "createdAt missing for $id"); + $this->assertNotEmpty($doc->getAttribute('$updatedAt'), "updatedAt missing for $id"); + } + + // Test 2: Bulk update with custom dates + $updateDoc = new Document([ + 'string' => 'updated', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ]); + $ids = []; + foreach ($documents as $doc) { + $ids[] = $doc->getId(); + } + $count = $database->updateDocuments($collection, $updateDoc, [ + Query::equal('$id', $ids) + ]); + $this->assertEquals(6, $count); + + foreach (['doc1', 'doc3'] as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertEquals($createDate, $doc->getAttribute('$createdAt'), "createdAt mismatch for $id"); + $this->assertEquals($updateDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); + $this->assertEquals('updated', $doc->getAttribute('string'), "string mismatch for $id"); } - foreach (['doc4', 'doc5'] as $id) { + foreach (['doc2', 'doc4','doc5','doc6'] as $id) { $doc = $database->getDocument($collection, $id); - $this->assertNotEmpty($doc->getAttribute('$createdAt'), "createdAt missing for doc: $id"); - $this->assertNotEmpty($doc->getAttribute('$updatedAt'), "updatedAt missing for doc: $id"); + $this->assertEquals($updateDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); + $this->assertEquals('updated', $doc->getAttribute('string'), "string mismatch for $id"); } + // Test 3: Bulk update with preserve dates disabled + $database->setPreserveDates(false); + + $customDate = 'should be ignored anyways so no error'; + $updateDocDisabled = new Document([ + 'string' => 'disabled_update', + '$createdAt' => $customDate, + '$updatedAt' => $customDate + ]); + + $countDisabled = $database->updateDocuments($collection, $updateDocDisabled); + $this->assertEquals(6, $countDisabled); + + // Test 4: Bulk update with preserve dates re-enabled + $database->setPreserveDates(true); + + $newDate = '2000-03-01T20:45:00.000+00:00'; + $updateDocEnabled = new Document([ + 'string' => 'enabled_update', + '$createdAt' => $newDate, + '$updatedAt' => $newDate + ]); + + $countEnabled = $database->updateDocuments($collection, $updateDocEnabled); + $this->assertEquals(6, $countEnabled); + $database->setPreserveDates(false); $database->deleteCollection($collection); } diff --git a/tests/e2e/Adapter/Scopes/GeneralTests.php b/tests/e2e/Adapter/Scopes/GeneralTests.php index 8ae6a8300..795f4d096 100644 --- a/tests/e2e/Adapter/Scopes/GeneralTests.php +++ b/tests/e2e/Adapter/Scopes/GeneralTests.php @@ -119,7 +119,38 @@ public function testPreserveDatesUpdate(): void '$permissions' => [], 'attr1' => 'value3', ])); + // updating with empty dates + try { + $doc1->setAttribute('$updatedAt', ''); + $doc1 = $database->updateDocument('preserve_update_dates', 'doc1', $doc1); + $this->fail('Failed to throw structure exception'); + + } catch (Exception $e) { + $this->assertInstanceOf(StructureException::class, $e); + $this->assertEquals('Invalid document structure: Missing required attribute "$updatedAt"', $e->getMessage()); + } + + try { + $this->getDatabase()->updateDocuments( + 'preserve_update_dates', + new Document([ + '$updatedAt' => '' + ]), + [ + Query::equal('$id', [ + $doc2->getId(), + $doc3->getId() + ]) + ] + ); + $this->fail('Failed to throw structure exception'); + + } catch (Exception $e) { + $this->assertInstanceOf(StructureException::class, $e); + $this->assertEquals('Invalid document structure: Missing required attribute "$updatedAt"', $e->getMessage()); + } + // non empty dates $newDate = '2000-01-01T10:00:00.000+00:00'; $doc1->setAttribute('$updatedAt', $newDate); From 51649917e51980217daece86edaf6d8b3c448739 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 31 Jul 2025 00:46:46 +0530 Subject: [PATCH 2/8] updated code ql --- src/Database/Validator/PartialStructure.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Database/Validator/PartialStructure.php b/src/Database/Validator/PartialStructure.php index 2fdff6c6e..660f8f46b 100644 --- a/src/Database/Validator/PartialStructure.php +++ b/src/Database/Validator/PartialStructure.php @@ -13,7 +13,7 @@ class PartialStructure extends Structure * Returns true if valid or false if not. * * @param mixed $document - * @param array $requiredAttributes optional list of required attributes to check + * @param array $requiredAttributes optional list of required attributes to check * * @return bool */ @@ -37,6 +37,9 @@ public function isValid($document, array $requiredAttributes = []): bool $name = $attribute['$id'] ?? ''; $keys[$name] = $attribute; } + /** + * @var array $requiredAttributesMap + */ $requiredAttributesMap = []; foreach ($this->attributes as $attribute) { if ($attribute['required'] === true && in_array($attribute['$id'], $requiredAttributes)) { From 478268461ca1288f67fcf7d106966ba74768cf4c Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 31 Jul 2025 13:19:48 +0530 Subject: [PATCH 3/8] added created at to be part of the update documents in sql if its not empty --- src/Database/Adapter/SQL.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index c90f4d720..936e32fb1 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -422,6 +422,10 @@ public function updateDocuments(string $collection, Document $updates, array $do $attributes['_updatedAt'] = $updates->getUpdatedAt(); } + if (!empty($updates->getCreatedAt())) { + $attributes['_createdAt'] = $updates->getCreatedAt(); + } + if (!empty($updates->getPermissions())) { $attributes['_permissions'] = json_encode($updates->getPermissions()); } From 5b5e2c5381380418883fe25469124fd103317c27 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 31 Jul 2025 13:20:02 +0530 Subject: [PATCH 4/8] Enhance upsert functionality to support custom createdAt and updatedAt attributes in DocumentTests --- src/Database/Database.php | 10 +- tests/e2e/Adapter/Scopes/DocumentTests.php | 221 +++++++++++++++++++++ 2 files changed, 226 insertions(+), 5 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index e1dfc39e2..ee150dfde 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4993,14 +4993,14 @@ public function createOrUpdateDocumentsWithIncrease( $document ->setAttribute('$id', empty($document->getId()) ? ID::unique() : $document->getId()) ->setAttribute('$collection', $collection->getId()) - ->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt) + ->setAttribute('$updatedAt', ($updatedAt === null || !$this->preserveDates) ? $time : $updatedAt) ->removeAttribute('$sequence'); - if ($old->isEmpty()) { - $createdAt = $document->getCreatedAt(); - $document->setAttribute('$createdAt', empty($createdAt) || !$this->preserveDates ? $time : $createdAt); + $createdAt = $document->getCreatedAt(); + if ($createdAt === null || !$this->preserveDates) { + $document->setAttribute('$createdAt', $old->isEmpty() ? $time : $old->getCreatedAt()); } else { - $document['$createdAt'] = $old->getCreatedAt(); + $document->setAttribute('$createdAt', $createdAt); } // Force matching optional parameter sets diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index e611c794d..dd09b0685 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -4834,6 +4834,102 @@ public function testNormalDocumentDateOperations(): void $this->assertNotEquals($customDate, $updatedDoc7->getAttribute('$createdAt')); $this->assertNotEquals($customDate, $updatedDoc7->getAttribute('$updatedAt')); + // Test 9: Upsert operations with custom dates + $database->setPreserveDates(true); + + // Test 9.1: Upsert new document with custom createdAt + $upsertResults = []; + $database->createOrUpdateDocuments($collection, [ + new Document([ + '$id' => 'upsert1', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'upsert1_initial', + '$createdAt' => $createDate + ]) + ], onNext: function ($doc) use (&$upsertResults) { + $upsertResults[] = $doc; + }); + $upsertDoc1 = $upsertResults[0]; + + $this->assertEquals($createDate, $upsertDoc1->getAttribute('$createdAt')); + $this->assertNotEquals($createDate, $upsertDoc1->getAttribute('$updatedAt')); + + // Test 9.2: Upsert existing document with custom updatedAt + $upsertDoc1->setAttribute('string', 'upsert1_updated'); + $upsertDoc1->setAttribute('$updatedAt', $updateDate); + $updatedUpsertResults = []; + $database->createOrUpdateDocuments($collection, [$upsertDoc1], onNext: function ($doc) use (&$updatedUpsertResults) { + $updatedUpsertResults[] = $doc; + }); + $updatedUpsertDoc1 = $updatedUpsertResults[0]; + + $this->assertEquals($createDate, $updatedUpsertDoc1->getAttribute('$createdAt')); + $this->assertEquals($updateDate, $updatedUpsertDoc1->getAttribute('$updatedAt')); + + // Test 9.3: Upsert new document with both custom dates + $upsertResults2 = []; + $database->createOrUpdateDocuments($collection, [ + new Document([ + '$id' => 'upsert2', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'upsert2_both_dates', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ]) + ], onNext: function ($doc) use (&$upsertResults2) { + $upsertResults2[] = $doc; + }); + $upsertDoc2 = $upsertResults2[0]; + + $this->assertEquals($createDate, $upsertDoc2->getAttribute('$createdAt')); + $this->assertEquals($updateDate, $upsertDoc2->getAttribute('$updatedAt')); + + // Test 9.4: Upsert existing document with different dates + $upsertDoc2->setAttribute('string', 'upsert2_updated'); + $upsertDoc2->setAttribute('$createdAt', $date3); + $upsertDoc2->setAttribute('$updatedAt', $date3); + $updatedUpsertResults2 = []; + $database->createOrUpdateDocuments($collection, [$upsertDoc2], onNext: function ($doc) use (&$updatedUpsertResults2) { + $updatedUpsertResults2[] = $doc; + }); + $updatedUpsertDoc2 = $updatedUpsertResults2[0]; + + $this->assertEquals($date3, $updatedUpsertDoc2->getAttribute('$createdAt')); + $this->assertEquals($date3, $updatedUpsertDoc2->getAttribute('$updatedAt')); + + // Test 9.5: Upsert with preserve dates disabled + $database->setPreserveDates(false); + + $upsertResults3 = []; + $database->createOrUpdateDocuments($collection, [ + new Document([ + '$id' => 'upsert3', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'upsert3_disabled', + '$createdAt' => $customDate, + '$updatedAt' => $customDate + ]) + ], onNext: function ($doc) use (&$upsertResults3) { + $upsertResults3[] = $doc; + }); + $upsertDoc3 = $upsertResults3[0]; + + $this->assertNotEquals($customDate, $upsertDoc3->getAttribute('$createdAt')); + $this->assertNotEquals($customDate, $upsertDoc3->getAttribute('$updatedAt')); + + // Update with custom dates should also be ignored + $upsertDoc3->setAttribute('string', 'upsert3_updated'); + $upsertDoc3->setAttribute('$createdAt', $customDate); + $upsertDoc3->setAttribute('$updatedAt', $customDate); + $updatedUpsertResults3 = []; + $database->createOrUpdateDocuments($collection, [$upsertDoc3], onNext: function ($doc) use (&$updatedUpsertResults3) { + $updatedUpsertResults3[] = $doc; + }); + $updatedUpsertDoc3 = $updatedUpsertResults3[0]; + + $this->assertNotEquals($customDate, $updatedUpsertDoc3->getAttribute('$createdAt')); + $this->assertNotEquals($customDate, $updatedUpsertDoc3->getAttribute('$updatedAt')); + $database->setPreserveDates(false); $database->deleteCollection($collection); } @@ -4965,6 +5061,131 @@ public function testBulkDocumentDateOperations(): void $countEnabled = $database->updateDocuments($collection, $updateDocEnabled); $this->assertEquals(6, $countEnabled); + // Test 5: Bulk upsert operations with custom dates + $database->setPreserveDates(true); + + // Test 5.1: Bulk upsert with different date configurations + $upsertDocuments = [ + new Document([ + '$id' => 'upsert1', + '$permissions' => $permissions, + 'string' => 'upsert1_initial', + '$createdAt' => $createDate + ]), + new Document([ + '$id' => 'upsert2', + '$permissions' => $permissions, + 'string' => 'upsert2_initial', + '$updatedAt' => $updateDate + ]), + new Document([ + '$id' => 'upsert3', + '$permissions' => $permissions, + 'string' => 'upsert3_initial', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ]), + new Document([ + '$id' => 'upsert4', + '$permissions' => $permissions, + 'string' => 'upsert4_initial' + ]) + ]; + + $upsertResults = []; + $database->createOrUpdateDocuments($collection, $upsertDocuments, onNext: function ($doc) use (&$upsertResults) { + $upsertResults[] = $doc; + }); + + // Verify initial upsert state + foreach (['upsert1', 'upsert3'] as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertEquals($createDate, $doc->getAttribute('$createdAt'), "createdAt mismatch for $id"); + } + + foreach (['upsert2', 'upsert3'] as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertEquals($updateDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); + } + + foreach (['upsert4'] as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertNotEmpty($doc->getAttribute('$createdAt'), "createdAt missing for $id"); + $this->assertNotEmpty($doc->getAttribute('$updatedAt'), "updatedAt missing for $id"); + } + + // Test 5.2: Bulk upsert update with custom dates using updateDocuments + $newDate = '2000-04-01T12:00:00.000+00:00'; + $updateUpsertDoc = new Document([ + 'string' => 'upsert_updated', + '$createdAt' => $newDate, + '$updatedAt' => $newDate + ]); + + $upsertIds = []; + foreach ($upsertDocuments as $doc) { + $upsertIds[] = $doc->getId(); + } + + $countUpsert = $database->updateDocuments($collection, $updateUpsertDoc, [ + Query::equal('$id', $upsertIds) + ]); + $this->assertEquals(4, $countUpsert); + + foreach ($upsertIds as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertEquals($newDate, $doc->getAttribute('$createdAt'), "createdAt mismatch for $id"); + $this->assertEquals($newDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); + $this->assertEquals('upsert_updated', $doc->getAttribute('string'), "string mismatch for $id"); + } + + // Test 5.3: Bulk upsert operations with createOrUpdateDocuments + $upsertUpdateDocuments = []; + foreach ($upsertDocuments as $doc) { + $updatedDoc = clone $doc; + $updatedDoc->setAttribute('string', 'upsert_updated_via_upsert'); + $updatedDoc->setAttribute('$createdAt', $newDate); + $updatedDoc->setAttribute('$updatedAt', $newDate); + $upsertUpdateDocuments[] = $updatedDoc; + } + + $upsertUpdateResults = []; + $countUpsertUpdate = $database->createOrUpdateDocuments($collection, $upsertUpdateDocuments, onNext: function ($doc) use (&$upsertUpdateResults) { + $upsertUpdateResults[] = $doc; + }); + $this->assertEquals(4, $countUpsertUpdate); + + foreach ($upsertUpdateResults as $doc) { + $this->assertEquals($newDate, $doc->getAttribute('$createdAt'), "createdAt mismatch for upsert update"); + $this->assertEquals($newDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for upsert update"); + $this->assertEquals('upsert_updated_via_upsert', $doc->getAttribute('string'), "string mismatch for upsert update"); + } + + // Test 5.4: Bulk upsert with preserve dates disabled + $database->setPreserveDates(false); + + $customDate = 'should be ignored anyways so no error'; + $upsertDisabledDocuments = []; + foreach ($upsertDocuments as $doc) { + $disabledDoc = clone $doc; + $disabledDoc->setAttribute('string', 'upsert_disabled'); + $disabledDoc->setAttribute('$createdAt', $customDate); + $disabledDoc->setAttribute('$updatedAt', $customDate); + $upsertDisabledDocuments[] = $disabledDoc; + } + + $upsertDisabledResults = []; + $countUpsertDisabled = $database->createOrUpdateDocuments($collection, $upsertDisabledDocuments, onNext: function ($doc) use (&$upsertDisabledResults) { + $upsertDisabledResults[] = $doc; + }); + $this->assertEquals(4, $countUpsertDisabled); + + foreach ($upsertDisabledResults as $doc) { + $this->assertNotEquals($customDate, $doc->getAttribute('$createdAt'), "createdAt should not be custom date when disabled"); + $this->assertNotEquals($customDate, $doc->getAttribute('$updatedAt'), "updatedAt should not be custom date when disabled"); + $this->assertEquals('upsert_disabled', $doc->getAttribute('string'), "string mismatch for disabled upsert"); + } + $database->setPreserveDates(false); $database->deleteCollection($collection); } From 484c4347d9b7e623a1eeee53cecd8415c48b4315 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 31 Jul 2025 13:26:33 +0530 Subject: [PATCH 5/8] updated structure unit test --- tests/unit/Validator/StructureTest.php | 60 +++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/tests/unit/Validator/StructureTest.php b/tests/unit/Validator/StructureTest.php index a9f641038..603256bfa 100644 --- a/tests/unit/Validator/StructureTest.php +++ b/tests/unit/Validator/StructureTest.php @@ -153,6 +153,8 @@ public function testCollection(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Collection not found', $validator->getDescription()); @@ -170,6 +172,8 @@ public function testRequiredKeys(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Missing required attribute "title"', $validator->getDescription()); @@ -188,6 +192,8 @@ public function testNullValues(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals(true, $validator->isValid(new Document([ @@ -199,6 +205,8 @@ public function testNullValues(): void 'published' => true, 'tags' => ['dog', null, 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); } @@ -216,6 +224,8 @@ public function testUnknownKeys(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Unknown attribute: "titlex"', $validator->getDescription()); @@ -234,6 +244,8 @@ public function testIntegerAsString(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "rating" has invalid type. Value must be a valid integer', $validator->getDescription()); @@ -252,6 +264,8 @@ public function testValidDocument(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); } @@ -268,6 +282,8 @@ public function testStringValidation(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "title" has invalid type. Value must be a valid string and no longer than 256 chars', $validator->getDescription()); @@ -286,6 +302,8 @@ public function testArrayOfStringsValidation(): void 'published' => true, 'tags' => [1, 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "tags[\'0\']" has invalid type. Value must be a valid string and no longer than 55 chars', $validator->getDescription()); @@ -299,6 +317,8 @@ public function testArrayOfStringsValidation(): void 'published' => true, 'tags' => [true], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "tags[\'0\']" has invalid type. Value must be a valid string and no longer than 55 chars', $validator->getDescription()); @@ -312,6 +332,8 @@ public function testArrayOfStringsValidation(): void 'published' => true, 'tags' => [], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals(false, $validator->isValid(new Document([ @@ -323,6 +345,8 @@ public function testArrayOfStringsValidation(): void 'published' => true, 'tags' => ['too-long-tag-name-to-make-sure-the-length-validator-inside-string-attribute-type-fails-properly'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "tags[\'0\']" has invalid type. Value must be a valid string and no longer than 55 chars', $validator->getDescription()); @@ -343,7 +367,9 @@ public function testArrayAsObjectValidation(): void 'price' => 1.99, 'published' => true, 'tags' => ['name' => 'dog'], - 'feedback' => 'team@appwrite.io' + 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); } @@ -359,7 +385,9 @@ public function testArrayOfObjectsValidation(): void 'price' => 1.99, 'published' => true, 'tags' => [['name' => 'dog']], - 'feedback' => 'team@appwrite.io' + 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); } @@ -376,6 +404,8 @@ public function testIntegerValidation(): void 'published' => false, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "rating" has invalid type. Value must be a valid integer', $validator->getDescription()); @@ -389,6 +419,8 @@ public function testIntegerValidation(): void 'published' => false, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "rating" has invalid type. Value must be a valid integer', $validator->getDescription()); @@ -408,6 +440,8 @@ public function testArrayOfIntegersValidation(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals(true, $validator->isValid(new Document([ @@ -420,6 +454,8 @@ public function testArrayOfIntegersValidation(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals(true, $validator->isValid(new Document([ @@ -432,6 +468,8 @@ public function testArrayOfIntegersValidation(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals(false, $validator->isValid(new Document([ @@ -444,6 +482,8 @@ public function testArrayOfIntegersValidation(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "reviews[\'0\']" has invalid type. Value must be a valid integer', $validator->getDescription()); @@ -462,6 +502,8 @@ public function testFloatValidation(): void 'published' => false, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "price" has invalid type. Value must be a valid float', $validator->getDescription()); @@ -475,6 +517,8 @@ public function testFloatValidation(): void 'published' => false, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "price" has invalid type. Value must be a valid float', $validator->getDescription()); @@ -493,6 +537,8 @@ public function testBooleanValidation(): void 'published' => 1, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "published" has invalid type. Value must be a valid boolean', $validator->getDescription()); @@ -506,6 +552,8 @@ public function testBooleanValidation(): void 'published' => '', 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "published" has invalid type. Value must be a valid boolean', $validator->getDescription()); @@ -524,6 +572,8 @@ public function testFormatValidation(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team_appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "feedback" has invalid format. Value must be a valid email address', $validator->getDescription()); @@ -542,6 +592,8 @@ public function testIntegerMaxRange(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "rating" has invalid type. Value must be a valid range between -2,147,483,647 and 2,147,483,647', $validator->getDescription()); @@ -560,6 +612,8 @@ public function testDoubleUnsigned(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertStringContainsString('Invalid document structure: Attribute "price" has invalid type. Value must be a valid range between 0 and ', $validator->getDescription()); @@ -578,6 +632,8 @@ public function testDoubleMaxRange(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); } From f5dd22da186ddaf39d75b0cedeac07852473f95f Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 31 Jul 2025 13:38:13 +0530 Subject: [PATCH 6/8] removed redundant param from the partial structure --- src/Database/Database.php | 4 +-- src/Database/Validator/PartialStructure.php | 14 ++++----- tests/e2e/Adapter/Scopes/DocumentTests.php | 33 +++++++++++++++++---- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index ee150dfde..1b42342a8 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4366,14 +4366,12 @@ public function updateDocuments( if (!empty($cursor) && $cursor->getCollection() !== $collection->getId()) { throw new DatabaseException("Cursor document must be from the same Collection."); } - $attributesToCheckForRequiredValidation = ['$updatedAt']; unset($updates['$id']); unset($updates['$tenant']); if (($updates->getCreatedAt() === null || !$this->preserveDates)) { unset($updates['$createdAt']); } else { $updates['$createdAt'] = $updates->getCreatedAt(); - $attributesToCheckForRequiredValidation[] = '$createdAt'; } if ($this->adapter->getSharedTables()) { $updates['$tenant'] = $this->adapter->getTenant(); @@ -4390,7 +4388,7 @@ public function updateDocuments( $this->adapter->getMaxDateTime(), ); - if (!$validator->isValid($updates, $attributesToCheckForRequiredValidation)) { + if (!$validator->isValid($updates)) { throw new StructureException($validator->getDescription()); } diff --git a/src/Database/Validator/PartialStructure.php b/src/Database/Validator/PartialStructure.php index 660f8f46b..08c38510f 100644 --- a/src/Database/Validator/PartialStructure.php +++ b/src/Database/Validator/PartialStructure.php @@ -13,11 +13,10 @@ class PartialStructure extends Structure * Returns true if valid or false if not. * * @param mixed $document - * @param array $requiredAttributes optional list of required attributes to check * * @return bool */ - public function isValid($document, array $requiredAttributes = []): bool + public function isValid($document): bool { if (!$document instanceof Document) { $this->message = 'Value must be an instance of Document'; @@ -37,17 +36,14 @@ public function isValid($document, array $requiredAttributes = []): bool $name = $attribute['$id'] ?? ''; $keys[$name] = $attribute; } - /** - * @var array $requiredAttributesMap - */ - $requiredAttributesMap = []; + $requiredAttributes = []; foreach ($this->attributes as $attribute) { - if ($attribute['required'] === true && in_array($attribute['$id'], $requiredAttributes)) { - $requiredAttributesMap[] = $attribute; + if ($attribute['required'] === true && $document->offsetExists($attribute['$id'])) { + $requiredAttributes[] = $attribute; } } - if (!$this->checkForAllRequiredValues($structure, $requiredAttributesMap, $keys)) { + if (!$this->checkForAllRequiredValues($structure, $requiredAttributes, $keys)) { return false; } if (!$this->checkForUnknownAttributes($structure, $keys)) { diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index dd09b0685..2c3086826 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -5064,7 +5064,7 @@ public function testBulkDocumentDateOperations(): void // Test 5: Bulk upsert operations with custom dates $database->setPreserveDates(true); - // Test 5.1: Bulk upsert with different date configurations + // Test 6: Bulk upsert with different date configurations $upsertDocuments = [ new Document([ '$id' => 'upsert1', @@ -5097,7 +5097,7 @@ public function testBulkDocumentDateOperations(): void $upsertResults[] = $doc; }); - // Verify initial upsert state + // Test 7: Verify initial upsert state foreach (['upsert1', 'upsert3'] as $id) { $doc = $database->getDocument($collection, $id); $this->assertEquals($createDate, $doc->getAttribute('$createdAt'), "createdAt mismatch for $id"); @@ -5114,7 +5114,7 @@ public function testBulkDocumentDateOperations(): void $this->assertNotEmpty($doc->getAttribute('$updatedAt'), "updatedAt missing for $id"); } - // Test 5.2: Bulk upsert update with custom dates using updateDocuments + // Test 8: Bulk upsert update with custom dates using updateDocuments $newDate = '2000-04-01T12:00:00.000+00:00'; $updateUpsertDoc = new Document([ 'string' => 'upsert_updated', @@ -5139,7 +5139,30 @@ public function testBulkDocumentDateOperations(): void $this->assertEquals('upsert_updated', $doc->getAttribute('string'), "string mismatch for $id"); } - // Test 5.3: Bulk upsert operations with createOrUpdateDocuments + // Test 9: checking by passing null to each + $updateUpsertDoc = new Document([ + 'string' => 'upsert_updated', + '$createdAt' => null, + '$updatedAt' => null + ]); + + $upsertIds = []; + foreach ($upsertDocuments as $doc) { + $upsertIds[] = $doc->getId(); + } + + $countUpsert = $database->updateDocuments($collection, $updateUpsertDoc, [ + Query::equal('$id', $upsertIds) + ]); + $this->assertEquals(4, $countUpsert); + + foreach ($upsertIds as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertNotEmpty($doc->getAttribute('$createdAt'), "createdAt mismatch for $id"); + $this->assertNotEmpty($doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); + } + + // Test 10: Bulk upsert operations with createOrUpdateDocuments $upsertUpdateDocuments = []; foreach ($upsertDocuments as $doc) { $updatedDoc = clone $doc; @@ -5161,7 +5184,7 @@ public function testBulkDocumentDateOperations(): void $this->assertEquals('upsert_updated_via_upsert', $doc->getAttribute('string'), "string mismatch for upsert update"); } - // Test 5.4: Bulk upsert with preserve dates disabled + // Test 11: Bulk upsert with preserve dates disabled $database->setPreserveDates(false); $customDate = 'should be ignored anyways so no error'; From cb43ebdb5ae9fc62bfc8087b8ed6bc803580a7b6 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 31 Jul 2025 14:33:22 +0530 Subject: [PATCH 7/8] updated php doc string --- src/Database/Validator/PartialStructure.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Database/Validator/PartialStructure.php b/src/Database/Validator/PartialStructure.php index 08c38510f..fd8f5a989 100644 --- a/src/Database/Validator/PartialStructure.php +++ b/src/Database/Validator/PartialStructure.php @@ -36,6 +36,9 @@ public function isValid($document): bool $name = $attribute['$id'] ?? ''; $keys[$name] = $attribute; } + /** + * @var array $requiredAttributes + */ $requiredAttributes = []; foreach ($this->attributes as $attribute) { if ($attribute['required'] === true && $document->offsetExists($attribute['$id'])) { From e15bf27f9576e96af26131d62bad8eb869356f92 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 31 Jul 2025 15:25:20 +0530 Subject: [PATCH 8/8] update --- src/Database/Database.php | 2 +- tests/e2e/Adapter/Scopes/DocumentTests.php | 278 +++++++++++---------- 2 files changed, 151 insertions(+), 129 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 1b42342a8..ad7210cdf 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4123,7 +4123,7 @@ public function updateDocument(string $collection, string $id, Document $documen $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; // Make sure user doesn't switch createdAt + $document['$createdAt'] = ($createdAt === null || !$this->preserveDates) ? $old->getCreatedAt() : $createdAt; if ($this->adapter->getSharedTables()) { $document['$tenant'] = $old->getTenant(); // Make sure user doesn't switch tenant diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 2c3086826..1d5208df9 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -4684,7 +4684,7 @@ public function testDateTimeDocument(): void $database->deleteCollection($collection); } - public function testNormalDocumentDateOperations(): void + public function testSingleDocumentDateOperations(): void { /** @var Database $database */ $database = static::getDatabase(); @@ -4834,102 +4834,6 @@ public function testNormalDocumentDateOperations(): void $this->assertNotEquals($customDate, $updatedDoc7->getAttribute('$createdAt')); $this->assertNotEquals($customDate, $updatedDoc7->getAttribute('$updatedAt')); - // Test 9: Upsert operations with custom dates - $database->setPreserveDates(true); - - // Test 9.1: Upsert new document with custom createdAt - $upsertResults = []; - $database->createOrUpdateDocuments($collection, [ - new Document([ - '$id' => 'upsert1', - '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], - 'string' => 'upsert1_initial', - '$createdAt' => $createDate - ]) - ], onNext: function ($doc) use (&$upsertResults) { - $upsertResults[] = $doc; - }); - $upsertDoc1 = $upsertResults[0]; - - $this->assertEquals($createDate, $upsertDoc1->getAttribute('$createdAt')); - $this->assertNotEquals($createDate, $upsertDoc1->getAttribute('$updatedAt')); - - // Test 9.2: Upsert existing document with custom updatedAt - $upsertDoc1->setAttribute('string', 'upsert1_updated'); - $upsertDoc1->setAttribute('$updatedAt', $updateDate); - $updatedUpsertResults = []; - $database->createOrUpdateDocuments($collection, [$upsertDoc1], onNext: function ($doc) use (&$updatedUpsertResults) { - $updatedUpsertResults[] = $doc; - }); - $updatedUpsertDoc1 = $updatedUpsertResults[0]; - - $this->assertEquals($createDate, $updatedUpsertDoc1->getAttribute('$createdAt')); - $this->assertEquals($updateDate, $updatedUpsertDoc1->getAttribute('$updatedAt')); - - // Test 9.3: Upsert new document with both custom dates - $upsertResults2 = []; - $database->createOrUpdateDocuments($collection, [ - new Document([ - '$id' => 'upsert2', - '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], - 'string' => 'upsert2_both_dates', - '$createdAt' => $createDate, - '$updatedAt' => $updateDate - ]) - ], onNext: function ($doc) use (&$upsertResults2) { - $upsertResults2[] = $doc; - }); - $upsertDoc2 = $upsertResults2[0]; - - $this->assertEquals($createDate, $upsertDoc2->getAttribute('$createdAt')); - $this->assertEquals($updateDate, $upsertDoc2->getAttribute('$updatedAt')); - - // Test 9.4: Upsert existing document with different dates - $upsertDoc2->setAttribute('string', 'upsert2_updated'); - $upsertDoc2->setAttribute('$createdAt', $date3); - $upsertDoc2->setAttribute('$updatedAt', $date3); - $updatedUpsertResults2 = []; - $database->createOrUpdateDocuments($collection, [$upsertDoc2], onNext: function ($doc) use (&$updatedUpsertResults2) { - $updatedUpsertResults2[] = $doc; - }); - $updatedUpsertDoc2 = $updatedUpsertResults2[0]; - - $this->assertEquals($date3, $updatedUpsertDoc2->getAttribute('$createdAt')); - $this->assertEquals($date3, $updatedUpsertDoc2->getAttribute('$updatedAt')); - - // Test 9.5: Upsert with preserve dates disabled - $database->setPreserveDates(false); - - $upsertResults3 = []; - $database->createOrUpdateDocuments($collection, [ - new Document([ - '$id' => 'upsert3', - '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], - 'string' => 'upsert3_disabled', - '$createdAt' => $customDate, - '$updatedAt' => $customDate - ]) - ], onNext: function ($doc) use (&$upsertResults3) { - $upsertResults3[] = $doc; - }); - $upsertDoc3 = $upsertResults3[0]; - - $this->assertNotEquals($customDate, $upsertDoc3->getAttribute('$createdAt')); - $this->assertNotEquals($customDate, $upsertDoc3->getAttribute('$updatedAt')); - - // Update with custom dates should also be ignored - $upsertDoc3->setAttribute('string', 'upsert3_updated'); - $upsertDoc3->setAttribute('$createdAt', $customDate); - $upsertDoc3->setAttribute('$updatedAt', $customDate); - $updatedUpsertResults3 = []; - $database->createOrUpdateDocuments($collection, [$upsertDoc3], onNext: function ($doc) use (&$updatedUpsertResults3) { - $updatedUpsertResults3[] = $doc; - }); - $updatedUpsertDoc3 = $updatedUpsertResults3[0]; - - $this->assertNotEquals($customDate, $updatedUpsertDoc3->getAttribute('$createdAt')); - $this->assertNotEquals($customDate, $updatedUpsertDoc3->getAttribute('$updatedAt')); - $database->setPreserveDates(false); $database->deleteCollection($collection); } @@ -5061,63 +4965,183 @@ public function testBulkDocumentDateOperations(): void $countEnabled = $database->updateDocuments($collection, $updateDocEnabled); $this->assertEquals(6, $countEnabled); - // Test 5: Bulk upsert operations with custom dates + $database->setPreserveDates(false); + $database->deleteCollection($collection); + } + + public function testUpsertDateOperations(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if (!$database->getAdapter()->getSupportForUpserts()) { + return; + } + + $collection = 'upsert_date_operations'; + $database->createCollection($collection); + $this->assertEquals(true, $database->createAttribute($collection, 'string', Database::VAR_STRING, 128, false)); + $database->setPreserveDates(true); - // Test 6: Bulk upsert with different date configurations - $upsertDocuments = [ + $createDate = '2000-01-01T10:00:00.000+00:00'; + $updateDate = '2000-02-01T15:30:00.000+00:00'; + $date1 = '2000-01-01T10:00:00.000+00:00'; + $date2 = '2000-02-01T15:30:00.000+00:00'; + $date3 = '2000-03-01T20:45:00.000+00:00'; + $permissions = [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())]; + + // Test 1: Upsert new document with custom createdAt + $upsertResults = []; + $database->createOrUpdateDocuments($collection, [ new Document([ '$id' => 'upsert1', '$permissions' => $permissions, 'string' => 'upsert1_initial', '$createdAt' => $createDate - ]), + ]) + ], onNext: function ($doc) use (&$upsertResults) { + $upsertResults[] = $doc; + }); + $upsertDoc1 = $upsertResults[0]; + + $this->assertEquals($createDate, $upsertDoc1->getAttribute('$createdAt')); + $this->assertNotEquals($createDate, $upsertDoc1->getAttribute('$updatedAt')); + + // Test 2: Upsert existing document with custom updatedAt + $upsertDoc1->setAttribute('string', 'upsert1_updated'); + $upsertDoc1->setAttribute('$updatedAt', $updateDate); + $updatedUpsertResults = []; + $database->createOrUpdateDocuments($collection, [$upsertDoc1], onNext: function ($doc) use (&$updatedUpsertResults) { + $updatedUpsertResults[] = $doc; + }); + $updatedUpsertDoc1 = $updatedUpsertResults[0]; + + $this->assertEquals($createDate, $updatedUpsertDoc1->getAttribute('$createdAt')); + $this->assertEquals($updateDate, $updatedUpsertDoc1->getAttribute('$updatedAt')); + + // Test 3: Upsert new document with both custom dates + $upsertResults2 = []; + $database->createOrUpdateDocuments($collection, [ new Document([ '$id' => 'upsert2', '$permissions' => $permissions, - 'string' => 'upsert2_initial', + 'string' => 'upsert2_both_dates', + '$createdAt' => $createDate, '$updatedAt' => $updateDate - ]), + ]) + ], onNext: function ($doc) use (&$upsertResults2) { + $upsertResults2[] = $doc; + }); + $upsertDoc2 = $upsertResults2[0]; + + $this->assertEquals($createDate, $upsertDoc2->getAttribute('$createdAt')); + $this->assertEquals($updateDate, $upsertDoc2->getAttribute('$updatedAt')); + + // Test 4: Upsert existing document with different dates + $upsertDoc2->setAttribute('string', 'upsert2_updated'); + $upsertDoc2->setAttribute('$createdAt', $date3); + $upsertDoc2->setAttribute('$updatedAt', $date3); + $updatedUpsertResults2 = []; + $database->createOrUpdateDocuments($collection, [$upsertDoc2], onNext: function ($doc) use (&$updatedUpsertResults2) { + $updatedUpsertResults2[] = $doc; + }); + $updatedUpsertDoc2 = $updatedUpsertResults2[0]; + + $this->assertEquals($date3, $updatedUpsertDoc2->getAttribute('$createdAt')); + $this->assertEquals($date3, $updatedUpsertDoc2->getAttribute('$updatedAt')); + + // Test 5: Upsert with preserve dates disabled + $database->setPreserveDates(false); + + $customDate = '2000-01-01T10:00:00.000+00:00'; + $upsertResults3 = []; + $database->createOrUpdateDocuments($collection, [ new Document([ '$id' => 'upsert3', '$permissions' => $permissions, - 'string' => 'upsert3_initial', + 'string' => 'upsert3_disabled', + '$createdAt' => $customDate, + '$updatedAt' => $customDate + ]) + ], onNext: function ($doc) use (&$upsertResults3) { + $upsertResults3[] = $doc; + }); + $upsertDoc3 = $upsertResults3[0]; + + $this->assertNotEquals($customDate, $upsertDoc3->getAttribute('$createdAt')); + $this->assertNotEquals($customDate, $upsertDoc3->getAttribute('$updatedAt')); + + // Update with custom dates should also be ignored + $upsertDoc3->setAttribute('string', 'upsert3_updated'); + $upsertDoc3->setAttribute('$createdAt', $customDate); + $upsertDoc3->setAttribute('$updatedAt', $customDate); + $updatedUpsertResults3 = []; + $database->createOrUpdateDocuments($collection, [$upsertDoc3], onNext: function ($doc) use (&$updatedUpsertResults3) { + $updatedUpsertResults3[] = $doc; + }); + $updatedUpsertDoc3 = $updatedUpsertResults3[0]; + + $this->assertNotEquals($customDate, $updatedUpsertDoc3->getAttribute('$createdAt')); + $this->assertNotEquals($customDate, $updatedUpsertDoc3->getAttribute('$updatedAt')); + + // Test 6: Bulk upsert operations with custom dates + $database->setPreserveDates(true); + + // Test 7: Bulk upsert with different date configurations + $upsertDocuments = [ + new Document([ + '$id' => 'bulk_upsert1', + '$permissions' => $permissions, + 'string' => 'bulk_upsert1_initial', + '$createdAt' => $createDate + ]), + new Document([ + '$id' => 'bulk_upsert2', + '$permissions' => $permissions, + 'string' => 'bulk_upsert2_initial', + '$updatedAt' => $updateDate + ]), + new Document([ + '$id' => 'bulk_upsert3', + '$permissions' => $permissions, + 'string' => 'bulk_upsert3_initial', '$createdAt' => $createDate, '$updatedAt' => $updateDate ]), new Document([ - '$id' => 'upsert4', + '$id' => 'bulk_upsert4', '$permissions' => $permissions, - 'string' => 'upsert4_initial' + 'string' => 'bulk_upsert4_initial' ]) ]; - $upsertResults = []; - $database->createOrUpdateDocuments($collection, $upsertDocuments, onNext: function ($doc) use (&$upsertResults) { - $upsertResults[] = $doc; + $bulkUpsertResults = []; + $database->createOrUpdateDocuments($collection, $upsertDocuments, onNext: function ($doc) use (&$bulkUpsertResults) { + $bulkUpsertResults[] = $doc; }); - // Test 7: Verify initial upsert state - foreach (['upsert1', 'upsert3'] as $id) { + // Test 8: Verify initial bulk upsert state + foreach (['bulk_upsert1', 'bulk_upsert3'] as $id) { $doc = $database->getDocument($collection, $id); $this->assertEquals($createDate, $doc->getAttribute('$createdAt'), "createdAt mismatch for $id"); } - foreach (['upsert2', 'upsert3'] as $id) { + foreach (['bulk_upsert2', 'bulk_upsert3'] as $id) { $doc = $database->getDocument($collection, $id); $this->assertEquals($updateDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); } - foreach (['upsert4'] as $id) { + foreach (['bulk_upsert4'] as $id) { $doc = $database->getDocument($collection, $id); $this->assertNotEmpty($doc->getAttribute('$createdAt'), "createdAt missing for $id"); $this->assertNotEmpty($doc->getAttribute('$updatedAt'), "updatedAt missing for $id"); } - // Test 8: Bulk upsert update with custom dates using updateDocuments + // Test 9: Bulk upsert update with custom dates using updateDocuments $newDate = '2000-04-01T12:00:00.000+00:00'; $updateUpsertDoc = new Document([ - 'string' => 'upsert_updated', + 'string' => 'bulk_upsert_updated', '$createdAt' => $newDate, '$updatedAt' => $newDate ]); @@ -5127,21 +5151,20 @@ public function testBulkDocumentDateOperations(): void $upsertIds[] = $doc->getId(); } - $countUpsert = $database->updateDocuments($collection, $updateUpsertDoc, [ + $database->updateDocuments($collection, $updateUpsertDoc, [ Query::equal('$id', $upsertIds) ]); - $this->assertEquals(4, $countUpsert); foreach ($upsertIds as $id) { $doc = $database->getDocument($collection, $id); $this->assertEquals($newDate, $doc->getAttribute('$createdAt'), "createdAt mismatch for $id"); $this->assertEquals($newDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); - $this->assertEquals('upsert_updated', $doc->getAttribute('string'), "string mismatch for $id"); + $this->assertEquals('bulk_upsert_updated', $doc->getAttribute('string'), "string mismatch for $id"); } - // Test 9: checking by passing null to each + // Test 10: checking by passing null to each $updateUpsertDoc = new Document([ - 'string' => 'upsert_updated', + 'string' => 'bulk_upsert_updated', '$createdAt' => null, '$updatedAt' => null ]); @@ -5151,10 +5174,9 @@ public function testBulkDocumentDateOperations(): void $upsertIds[] = $doc->getId(); } - $countUpsert = $database->updateDocuments($collection, $updateUpsertDoc, [ + $database->updateDocuments($collection, $updateUpsertDoc, [ Query::equal('$id', $upsertIds) ]); - $this->assertEquals(4, $countUpsert); foreach ($upsertIds as $id) { $doc = $database->getDocument($collection, $id); @@ -5162,11 +5184,11 @@ public function testBulkDocumentDateOperations(): void $this->assertNotEmpty($doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); } - // Test 10: Bulk upsert operations with createOrUpdateDocuments + // Test 11: Bulk upsert operations with createOrUpdateDocuments $upsertUpdateDocuments = []; foreach ($upsertDocuments as $doc) { $updatedDoc = clone $doc; - $updatedDoc->setAttribute('string', 'upsert_updated_via_upsert'); + $updatedDoc->setAttribute('string', 'bulk_upsert_updated_via_upsert'); $updatedDoc->setAttribute('$createdAt', $newDate); $updatedDoc->setAttribute('$updatedAt', $newDate); $upsertUpdateDocuments[] = $updatedDoc; @@ -5181,17 +5203,17 @@ public function testBulkDocumentDateOperations(): void foreach ($upsertUpdateResults as $doc) { $this->assertEquals($newDate, $doc->getAttribute('$createdAt'), "createdAt mismatch for upsert update"); $this->assertEquals($newDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for upsert update"); - $this->assertEquals('upsert_updated_via_upsert', $doc->getAttribute('string'), "string mismatch for upsert update"); + $this->assertEquals('bulk_upsert_updated_via_upsert', $doc->getAttribute('string'), "string mismatch for upsert update"); } - // Test 11: Bulk upsert with preserve dates disabled + // Test 12: Bulk upsert with preserve dates disabled $database->setPreserveDates(false); $customDate = 'should be ignored anyways so no error'; $upsertDisabledDocuments = []; foreach ($upsertDocuments as $doc) { $disabledDoc = clone $doc; - $disabledDoc->setAttribute('string', 'upsert_disabled'); + $disabledDoc->setAttribute('string', 'bulk_upsert_disabled'); $disabledDoc->setAttribute('$createdAt', $customDate); $disabledDoc->setAttribute('$updatedAt', $customDate); $upsertDisabledDocuments[] = $disabledDoc; @@ -5206,7 +5228,7 @@ public function testBulkDocumentDateOperations(): void foreach ($upsertDisabledResults as $doc) { $this->assertNotEquals($customDate, $doc->getAttribute('$createdAt'), "createdAt should not be custom date when disabled"); $this->assertNotEquals($customDate, $doc->getAttribute('$updatedAt'), "updatedAt should not be custom date when disabled"); - $this->assertEquals('upsert_disabled', $doc->getAttribute('string'), "string mismatch for disabled upsert"); + $this->assertEquals('bulk_upsert_disabled', $doc->getAttribute('string'), "string mismatch for disabled upsert"); } $database->setPreserveDates(false);