From 42eca73d08d9d77fd1f48d3b36e628814c6a69aa Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 25 Sep 2025 18:59:24 +0530 Subject: [PATCH 01/15] updated index validator --- src/Database/Validator/Index.php | 4 +++ tests/unit/Validator/IndexTest.php | 41 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index e22bfaaff..a9df576eb 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -203,6 +203,10 @@ public function checkIndexLength(Document $index): bool return true; } + if (!$this->supportForAttributes) { + return true; + } + $total = 0; $lengths = $index->getAttribute('lengths', []); $attributes = $index->getAttribute('attributes', []); diff --git a/tests/unit/Validator/IndexTest.php b/tests/unit/Validator/IndexTest.php index a2862830c..0e76abe47 100644 --- a/tests/unit/Validator/IndexTest.php +++ b/tests/unit/Validator/IndexTest.php @@ -349,4 +349,45 @@ public function testReservedIndexKey(): void $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); } + + /** + * @throws Exception + */ + public function testIndexWithNoAttributeSupport() + { + $collection = new Document([ + '$id' => ID::custom('test'), + 'name' => 'test', + 'attributes' => [ + new Document([ + '$id' => ID::custom('title'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 769, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ]), + ], + 'indexes' => [ + new Document([ + '$id' => ID::custom('index1'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['new'], + 'lengths' => [], + 'orders' => [], + ]), + ], + ]); + + $validator = new Index($collection->getAttribute('attributes'), 768); + $index = $collection->getAttribute('indexes')[0]; + $this->assertFalse($validator->isValid($index)); + + $validator = new Index($collection->getAttribute('attributes'), 768, supportForAttributes:false); + $index = $collection->getAttribute('indexes')[0]; + $this->assertTrue($validator->isValid($index)); + } } From 8d2583e15385eeba48fe164bf9eca05b1c11174b Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 25 Sep 2025 18:59:50 +0530 Subject: [PATCH 02/15] updated selection attributes for schemales --- src/Database/Database.php | 11 ++--- src/Database/Validator/Queries/Document.php | 5 ++- tests/e2e/Adapter/Scopes/DocumentTests.php | 47 +++++++++++++++++++++ 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 27dc05352..2280eb012 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3470,7 +3470,7 @@ public function getDocument(string $collection, string $id, array $queries = [], $this->checkQueriesType($queries); if ($this->validate) { - $validator = new DocumentValidator($attributes); + $validator = new DocumentValidator($attributes, $this->adapter->getSupportForAttributes()); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } @@ -7062,10 +7062,11 @@ private function validateSelections(Document $collection, array $queries): array $keys[] = $attribute['key'] ?? $attribute['$id']; } } - - $invalid = \array_diff($selections, $keys); - if (!empty($invalid) && !\in_array('*', $invalid)) { - throw new QueryException('Cannot select attributes: ' . \implode(', ', $invalid)); + if ($this->adapter->getSupportForAttributes()) { + $invalid = \array_diff($selections, $keys); + if (!empty($invalid) && !\in_array('*', $invalid)) { + throw new QueryException('Cannot select attributes: ' . \implode(', ', $invalid)); + } } $selections = \array_merge($selections, $relationshipSelections); diff --git a/src/Database/Validator/Queries/Document.php b/src/Database/Validator/Queries/Document.php index 41c9f3f9b..953e32bbf 100644 --- a/src/Database/Validator/Queries/Document.php +++ b/src/Database/Validator/Queries/Document.php @@ -11,9 +11,10 @@ class Document extends Queries { /** * @param array $attributes + * @param bool $supportForAttributes * @throws Exception */ - public function __construct(array $attributes) + public function __construct(array $attributes, $supportForAttributes = true) { $attributes[] = new \Utopia\Database\Document([ '$id' => '$id', @@ -35,7 +36,7 @@ public function __construct(array $attributes) ]); $validators = [ - new Select($attributes), + new Select($attributes, $supportForAttributes), ]; parent::__construct($validators); diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index b10e345d6..e90d5fc27 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -6550,4 +6550,51 @@ public function testSchemaEnforcedDocumentCreation(): void $database->deleteCollection($colName); } + + public function testSchemalessSelectionOnUnknownAttributes() + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $colName = uniqid("schemaless"); + $database->createCollection($colName); + $permissions = [Permission::read(Role::any()), Permission::write(Role::any()), Permission::update(Role::any())]; + $docs = [ + new Document(['$id' => 'doc1', '$permissions' => $permissions, 'freeA' => 'doc1']), + new Document(['$id' => 'doc2', '$permissions' => $permissions, 'freeB' => 'test']), + new Document(['$id' => 'doc3', '$permissions' => $permissions]), + ]; + $this->assertEquals(3, $database->createDocuments($colName, $docs)); + + $docA = $database->getDocument($colName, 'doc1', [Query::select(['freeA'])]); + $this->assertEquals('doc1', $docA->getAttribute('freeA')); + + $docC = $database->getDocument($colName, 'doc1', [Query::select(['freeC'])]); + $this->assertNull($docC->getAttribute('freeC')); + + $docs = $database->find($colName, [Query::equal('$id', ['doc1','doc2']),Query::select(['freeC'])]); + foreach ($docs as $doc) { + $this->assertNull($doc->getAttribute('freeC')); + // since not selected + $this->assertNull($doc->getAttribute('freeA')); + $this->assertNull($doc->getAttribute('freeB')); + } + + $docA = $database->find($colName, [ + Query::equal('$id', ['doc1']), + Query::select(['freeA']) + ]); + $this->assertEquals('doc1', $docA[0]->getAttribute('freeA')); + + $docC = $database->find($colName, [ + Query::equal('$id', ['doc1']), + Query::select(['freeC']) + ]); + $this->assertNull($docC[0]->getAttribute('freeC')); + } } From ef7363a7f78ee943d83a505281ae12c6945e6304 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 25 Sep 2025 19:41:10 +0530 Subject: [PATCH 03/15] linting --- src/Database/Database.php | 6 +++++- tests/e2e/Adapter/Scopes/DocumentTests.php | 2 +- tests/unit/Validator/IndexTest.php | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 2280eb012..f075ac9aa 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4661,6 +4661,7 @@ public function updateDocuments( $this->maxQueryValues, $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes() ); if (!$validator->isValid($queries)) { @@ -6207,7 +6208,8 @@ public function deleteDocuments( $this->adapter->getIdAttributeType(), $this->maxQueryValues, $this->adapter->getMinDateTime(), - $this->adapter->getMaxDateTime() + $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes() ); if (!$validator->isValid($queries)) { @@ -6626,6 +6628,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) $this->maxQueryValues, $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes() ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); @@ -6677,6 +6680,7 @@ public function sum(string $collection, string $attribute, array $queries = [], $this->maxQueryValues, $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes() ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index e90d5fc27..bdb523c8a 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -6551,7 +6551,7 @@ public function testSchemaEnforcedDocumentCreation(): void $database->deleteCollection($colName); } - public function testSchemalessSelectionOnUnknownAttributes() + public function testSchemalessSelectionOnUnknownAttributes(): void { /** @var Database $database */ $database = static::getDatabase(); diff --git a/tests/unit/Validator/IndexTest.php b/tests/unit/Validator/IndexTest.php index 0e76abe47..b915c8c34 100644 --- a/tests/unit/Validator/IndexTest.php +++ b/tests/unit/Validator/IndexTest.php @@ -353,7 +353,7 @@ public function testReservedIndexKey(): void /** * @throws Exception */ - public function testIndexWithNoAttributeSupport() + public function testIndexWithNoAttributeSupport(): void { $collection = new Document([ '$id' => ID::custom('test'), From 6a36761adf6973727b58a465171da95f9b2fe171 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 26 Sep 2025 13:40:25 +0530 Subject: [PATCH 04/15] * updated increment decrement db operation * scoped schemaless tests to a seprate test file --- src/Database/Database.php | 65 +- tests/e2e/Adapter/Base.php | 2 + tests/e2e/Adapter/Scopes/DocumentTests.php | 215 ----- tests/e2e/Adapter/Scopes/SchemalessTests.php | 967 +++++++++++++++++++ 4 files changed, 1003 insertions(+), 246 deletions(-) create mode 100644 tests/e2e/Adapter/Scopes/SchemalessTests.php diff --git a/src/Database/Database.php b/src/Database/Database.php index f075ac9aa..da6c00ff3 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -5537,24 +5537,25 @@ public function increaseDocumentAttribute( } $collection = $this->silent(fn () => $this->getCollection($collection)); + if ($this->adapter->getSupportForAttributes()) { + $attr = \array_filter($collection->getAttribute('attributes', []), function ($a) use ($attribute) { + return $a['$id'] === $attribute; + }); - $attr = \array_filter($collection->getAttribute('attributes', []), function ($a) use ($attribute) { - return $a['$id'] === $attribute; - }); - - if (empty($attr)) { - throw new NotFoundException('Attribute not found'); - } + if (empty($attr)) { + throw new NotFoundException('Attribute not found'); + } - $whiteList = [ - self::VAR_INTEGER, - self::VAR_FLOAT - ]; + $whiteList = [ + self::VAR_INTEGER, + self::VAR_FLOAT + ]; - /** @var Document $attr */ - $attr = \end($attr); - if (!\in_array($attr->getAttribute('type'), $whiteList) || $attr->getAttribute('array')) { - throw new TypeException('Attribute must be an integer or float and can not be an array.'); + /** @var Document $attr */ + $attr = \end($attr); + if (!\in_array($attr->getAttribute('type'), $whiteList) || $attr->getAttribute('array')) { + throw new TypeException('Attribute must be an integer or float and can not be an array.'); + } } $document = $this->withTransaction(function () use ($collection, $id, $attribute, $value, $max) { @@ -5635,25 +5636,27 @@ public function decreaseDocumentAttribute( $collection = $this->silent(fn () => $this->getCollection($collection)); - $attr = \array_filter($collection->getAttribute('attributes', []), function ($a) use ($attribute) { - return $a['$id'] === $attribute; - }); + if ($this->adapter->getSupportForAttributes()) { + $attr = \array_filter($collection->getAttribute('attributes', []), function ($a) use ($attribute) { + return $a['$id'] === $attribute; + }); - if (empty($attr)) { - throw new NotFoundException('Attribute not found'); - } + if (empty($attr)) { + throw new NotFoundException('Attribute not found'); + } - $whiteList = [ - self::VAR_INTEGER, - self::VAR_FLOAT - ]; + $whiteList = [ + self::VAR_INTEGER, + self::VAR_FLOAT + ]; - /** - * @var Document $attr - */ - $attr = \end($attr); - if (!\in_array($attr->getAttribute('type'), $whiteList) || $attr->getAttribute('array')) { - throw new TypeException('Attribute must be an integer or float and can not be an array.'); + /** + * @var Document $attr + */ + $attr = \end($attr); + if (!\in_array($attr->getAttribute('type'), $whiteList) || $attr->getAttribute('array')) { + throw new TypeException('Attribute must be an integer or float and can not be an array.'); + } } $document = $this->withTransaction(function () use ($collection, $id, $attribute, $value, $min) { diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 37ad7cce3..7d24a8f33 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -10,6 +10,7 @@ use Tests\E2E\Adapter\Scopes\IndexTests; use Tests\E2E\Adapter\Scopes\PermissionTests; use Tests\E2E\Adapter\Scopes\RelationshipTests; +use Tests\E2E\Adapter\Scopes\SchemalessTests; use Tests\E2E\Adapter\Scopes\SpatialTests; use Utopia\Database\Database; use Utopia\Database\Validator\Authorization; @@ -25,6 +26,7 @@ abstract class Base extends TestCase use PermissionTests; use RelationshipTests; use SpatialTests; + use SchemalessTests; use GeneralTests; protected static string $namespace; diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index bdb523c8a..974cefa4b 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -6382,219 +6382,4 @@ function (mixed $value) { return str_replace('prefix_', '', $value); } $database->deleteCollection($collectionId); } - - public function testSchemalessDocumentOperation(): void - { - /** @var Database $database */ - $database = static::getDatabase(); - - if ($database->getAdapter()->getSupportForAttributes()) { - $this->expectNotToPerformAssertions(); - return; - } - - $colName = uniqid("schemaless"); - $database->createCollection($colName); - $database->createAttribute($colName, 'key', Database::VAR_STRING, 50, true); - $database->createAttribute($colName, 'value', Database::VAR_STRING, 50, false, 'value'); - - $permissions = [Permission::read(Role::any()), Permission::write(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())]; - - // Valid documents without any predefined attributes - $docs = [ - new Document(['$id' => 'doc1', '$permissions' => $permissions, 'freeA' => 'doc1']), - new Document(['$id' => 'doc2', '$permissions' => $permissions, 'freeB' => 'test']), - new Document(['$id' => 'doc3', '$permissions' => $permissions]), - ]; - $this->assertEquals(3, $database->createDocuments($colName, $docs)); - - // Any extra attributes should be allowed (fully schemaless) - $docs = [ - new Document(['$id' => 'doc11', 'title' => 'doc1', '$permissions' => $permissions]), - new Document(['$id' => 'doc21', 'moviename' => 'doc2', 'moviedescription' => 'test', '$permissions' => $permissions]), - new Document(['$id' => 'doc31', '$permissions' => $permissions]), - ]; - - $createdDocs = $database->createDocuments($colName, $docs); - $this->assertEquals(3, $createdDocs); - - // Create a single document with extra attribute as well - $single = $database->createDocument($colName, new Document(['$id' => 'docS', 'extra' => 'yes', '$permissions' => $permissions])); - $this->assertEquals('docS', $single->getId()); - $this->assertEquals('yes', $single->getAttribute('extra')); - - $found = $database->find($colName); - $this->assertCount(7, $found); - $doc11 = $database->getDocument($colName, 'doc11'); - $this->assertEquals('doc1', $doc11->getAttribute('title')); - - $doc21 = $database->getDocument($colName, 'doc21'); - $this->assertEquals('doc2', $doc21->getAttribute('moviename')); - $this->assertEquals('test', $doc21->getAttribute('moviedescription')); - - $updated = $database->updateDocument($colName, 'doc31', new Document(['moviename' => 'updated'])) - ; - $this->assertEquals('updated', $updated->getAttribute('moviename')); - - $this->assertTrue($database->deleteDocument($colName, 'doc21')); - $deleted = $database->getDocument($colName, 'doc21'); - $this->assertTrue($deleted->isEmpty()); - $remaining = $database->find($colName); - $this->assertCount(6, $remaining); - - // Bulk update: set a new extra attribute on all remaining docs - $modified = $database->updateDocuments($colName, new Document(['bulkExtra' => 'yes'])); - $this->assertEquals(6, $modified); - $all = $database->find($colName); - foreach ($all as $doc) { - $this->assertEquals('yes', $doc->getAttribute('bulkExtra')); - } - - // Upsert: create new and update existing with extra attributes preserved - $upserts = [ - new Document(['$id' => 'docU1', 'extraU' => 1, '$permissions' => $permissions]), - new Document(['$id' => 'doc1', 'extraU' => 2, '$permissions' => $permissions]), - ]; - $countUpserts = $database->upsertDocuments($colName, $upserts); - $this->assertEquals(2, $countUpserts); - $docU1 = $database->getDocument($colName, 'docU1'); - $this->assertEquals(1, $docU1->getAttribute('extraU')); - $doc1AfterUpsert = $database->getDocument($colName, 'doc1'); - $this->assertEquals(2, $doc1AfterUpsert->getAttribute('extraU')); - - // Increase/Decrease numeric attribute: add numeric attribute and mutate it - $database->createAttribute($colName, 'counter', Database::VAR_INTEGER, 0, false, 0); - $docS = $database->getDocument($colName, 'docS'); - $this->assertEquals(0, $docS->getAttribute('counter')); - $docS = $database->increaseDocumentAttribute($colName, 'docS', 'counter', 5); - $this->assertEquals(5, $docS->getAttribute('counter')); - $docS = $database->decreaseDocumentAttribute($colName, 'docS', 'counter', 3); - $this->assertEquals(2, $docS->getAttribute('counter')); - - $deletedByCounter = $database->deleteDocuments($colName, [Query::equal('counter', [2])]); - $this->assertEquals(1, $deletedByCounter); - - $deletedCount = $database->deleteDocuments($colName, [Query::startsWith('$id', 'doc')]); - $this->assertEquals(6, $deletedCount); - $postDelete = $database->find($colName); - $this->assertCount(0, $postDelete); - - $database->deleteCollection($colName); - } - - public function testSchemalessDocumentInvalidInteralAttributeValidation(): void - { - /** @var Database $database */ - $database = static::getDatabase(); - - // test to ensure internal attributes are checked during creating schemaless document - if ($database->getAdapter()->getSupportForAttributes()) { - $this->expectNotToPerformAssertions(); - return; - } - - $colName = uniqid("schemaless"); - $database->createCollection($colName); - try { - $docs = [ - new Document(['$id' => true, 'freeA' => 'doc1']), - new Document(['$id' => true, 'freeB' => 'test']), - new Document(['$id' => true]), - ]; - $database->createDocuments($colName, $docs); - } catch (\Throwable $e) { - $this->assertInstanceOf(StructureException::class, $e); - } - - try { - $docs = [ - new Document(['$createdAt' => true, 'freeA' => 'doc1']), - new Document(['$updatedAt' => true, 'freeB' => 'test']), - new Document(['$permissions' => 12]), - ]; - $database->createDocuments($colName, $docs); - } catch (\Throwable $e) { - $this->assertInstanceOf(StructureException::class, $e); - } - - $database->deleteCollection($colName); - - } - - public function testSchemaEnforcedDocumentCreation(): void - { - /** @var Database $database */ - $database = static::getDatabase(); - - if (!$database->getAdapter()->getSupportForAttributes()) { - $this->expectNotToPerformAssertions(); - return; - } - - $colName = uniqid("schema"); - $database->createCollection($colName); - $database->createAttribute($colName, 'key', Database::VAR_STRING, 50, true); - $database->createAttribute($colName, 'value', Database::VAR_STRING, 50, false, 'value'); - - $permissions = [Permission::read(Role::any()), Permission::write(Role::any()), Permission::update(Role::any())]; - - // Extra attributes should fail - $docs = [ - new Document(['$id' => 'doc11', 'key' => 'doc1', 'title' => 'doc1', '$permissions' => $permissions]), - new Document(['$id' => 'doc21', 'key' => 'doc2', 'moviename' => 'doc2', 'moviedescription' => 'test', '$permissions' => $permissions]), - new Document(['$id' => 'doc31', 'key' => 'doc3', '$permissions' => $permissions]), - ]; - - $this->expectException(StructureException::class); - $database->createDocuments($colName, $docs); - - $database->deleteCollection($colName); - } - - public function testSchemalessSelectionOnUnknownAttributes(): void - { - /** @var Database $database */ - $database = static::getDatabase(); - - if ($database->getAdapter()->getSupportForAttributes()) { - $this->expectNotToPerformAssertions(); - return; - } - - $colName = uniqid("schemaless"); - $database->createCollection($colName); - $permissions = [Permission::read(Role::any()), Permission::write(Role::any()), Permission::update(Role::any())]; - $docs = [ - new Document(['$id' => 'doc1', '$permissions' => $permissions, 'freeA' => 'doc1']), - new Document(['$id' => 'doc2', '$permissions' => $permissions, 'freeB' => 'test']), - new Document(['$id' => 'doc3', '$permissions' => $permissions]), - ]; - $this->assertEquals(3, $database->createDocuments($colName, $docs)); - - $docA = $database->getDocument($colName, 'doc1', [Query::select(['freeA'])]); - $this->assertEquals('doc1', $docA->getAttribute('freeA')); - - $docC = $database->getDocument($colName, 'doc1', [Query::select(['freeC'])]); - $this->assertNull($docC->getAttribute('freeC')); - - $docs = $database->find($colName, [Query::equal('$id', ['doc1','doc2']),Query::select(['freeC'])]); - foreach ($docs as $doc) { - $this->assertNull($doc->getAttribute('freeC')); - // since not selected - $this->assertNull($doc->getAttribute('freeA')); - $this->assertNull($doc->getAttribute('freeB')); - } - - $docA = $database->find($colName, [ - Query::equal('$id', ['doc1']), - Query::select(['freeA']) - ]); - $this->assertEquals('doc1', $docA[0]->getAttribute('freeA')); - - $docC = $database->find($colName, [ - Query::equal('$id', ['doc1']), - Query::select(['freeC']) - ]); - $this->assertNull($docC[0]->getAttribute('freeC')); - } } diff --git a/tests/e2e/Adapter/Scopes/SchemalessTests.php b/tests/e2e/Adapter/Scopes/SchemalessTests.php new file mode 100644 index 000000000..dd40fd21e --- /dev/null +++ b/tests/e2e/Adapter/Scopes/SchemalessTests.php @@ -0,0 +1,967 @@ +getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $colName = uniqid("schemaless"); + $database->createCollection($colName); + $database->createAttribute($colName, 'key', Database::VAR_STRING, 50, true); + $database->createAttribute($colName, 'value', Database::VAR_STRING, 50, false, 'value'); + + $permissions = [Permission::read(Role::any()), Permission::write(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())]; + + // Valid documents without any predefined attributes + $docs = [ + new Document(['$id' => 'doc1', '$permissions' => $permissions, 'freeA' => 'doc1']), + new Document(['$id' => 'doc2', '$permissions' => $permissions, 'freeB' => 'test']), + new Document(['$id' => 'doc3', '$permissions' => $permissions]), + ]; + $this->assertEquals(3, $database->createDocuments($colName, $docs)); + + // Any extra attributes should be allowed (fully schemaless) + $docs = [ + new Document(['$id' => 'doc11', 'title' => 'doc1', '$permissions' => $permissions]), + new Document(['$id' => 'doc21', 'moviename' => 'doc2', 'moviedescription' => 'test', '$permissions' => $permissions]), + new Document(['$id' => 'doc31', '$permissions' => $permissions]), + ]; + + $createdDocs = $database->createDocuments($colName, $docs); + $this->assertEquals(3, $createdDocs); + + // Create a single document with extra attribute as well + $single = $database->createDocument($colName, new Document(['$id' => 'docS', 'extra' => 'yes', '$permissions' => $permissions])); + $this->assertEquals('docS', $single->getId()); + $this->assertEquals('yes', $single->getAttribute('extra')); + + $found = $database->find($colName); + $this->assertCount(7, $found); + $doc11 = $database->getDocument($colName, 'doc11'); + $this->assertEquals('doc1', $doc11->getAttribute('title')); + + $doc21 = $database->getDocument($colName, 'doc21'); + $this->assertEquals('doc2', $doc21->getAttribute('moviename')); + $this->assertEquals('test', $doc21->getAttribute('moviedescription')); + + $updated = $database->updateDocument($colName, 'doc31', new Document(['moviename' => 'updated'])) + ; + $this->assertEquals('updated', $updated->getAttribute('moviename')); + + $this->assertTrue($database->deleteDocument($colName, 'doc21')); + $deleted = $database->getDocument($colName, 'doc21'); + $this->assertTrue($deleted->isEmpty()); + $remaining = $database->find($colName); + $this->assertCount(6, $remaining); + + // Bulk update: set a new extra attribute on all remaining docs + $modified = $database->updateDocuments($colName, new Document(['bulkExtra' => 'yes'])); + $this->assertEquals(6, $modified); + $all = $database->find($colName); + foreach ($all as $doc) { + $this->assertEquals('yes', $doc->getAttribute('bulkExtra')); + } + + // Upsert: create new and update existing with extra attributes preserved + $upserts = [ + new Document(['$id' => 'docU1', 'extraU' => 1, '$permissions' => $permissions]), + new Document(['$id' => 'doc1', 'extraU' => 2, '$permissions' => $permissions]), + ]; + $countUpserts = $database->upsertDocuments($colName, $upserts); + $this->assertEquals(2, $countUpserts); + $docU1 = $database->getDocument($colName, 'docU1'); + $this->assertEquals(1, $docU1->getAttribute('extraU')); + $doc1AfterUpsert = $database->getDocument($colName, 'doc1'); + $this->assertEquals(2, $doc1AfterUpsert->getAttribute('extraU')); + + // Increase/Decrease numeric attribute: add numeric attribute and mutate it + $database->createAttribute($colName, 'counter', Database::VAR_INTEGER, 0, false, 0); + $docS = $database->getDocument($colName, 'docS'); + $this->assertEquals(0, $docS->getAttribute('counter')); + $docS = $database->increaseDocumentAttribute($colName, 'docS', 'counter', 5); + $this->assertEquals(5, $docS->getAttribute('counter')); + $docS = $database->decreaseDocumentAttribute($colName, 'docS', 'counter', 3); + $this->assertEquals(2, $docS->getAttribute('counter')); + + $deletedByCounter = $database->deleteDocuments($colName, [Query::equal('counter', [2])]); + $this->assertEquals(1, $deletedByCounter); + + $deletedCount = $database->deleteDocuments($colName, [Query::startsWith('$id', 'doc')]); + $this->assertEquals(6, $deletedCount); + $postDelete = $database->find($colName); + $this->assertCount(0, $postDelete); + + $database->deleteCollection($colName); + } + + public function testSchemalessDocumentInvalidInteralAttributeValidation(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + // test to ensure internal attributes are checked during creating schemaless document + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $colName = uniqid("schemaless"); + $database->createCollection($colName); + try { + $docs = [ + new Document(['$id' => true, 'freeA' => 'doc1']), + new Document(['$id' => true, 'freeB' => 'test']), + new Document(['$id' => true]), + ]; + $database->createDocuments($colName, $docs); + } catch (\Throwable $e) { + $this->assertInstanceOf(StructureException::class, $e); + } + + try { + $docs = [ + new Document(['$createdAt' => true, 'freeA' => 'doc1']), + new Document(['$updatedAt' => true, 'freeB' => 'test']), + new Document(['$permissions' => 12]), + ]; + $database->createDocuments($colName, $docs); + } catch (\Throwable $e) { + $this->assertInstanceOf(StructureException::class, $e); + } + + $database->deleteCollection($colName); + + } + + public function testSchemaEnforcedDocumentCreation(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if (!$database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $colName = uniqid("schema"); + $database->createCollection($colName); + $database->createAttribute($colName, 'key', Database::VAR_STRING, 50, true); + $database->createAttribute($colName, 'value', Database::VAR_STRING, 50, false, 'value'); + + $permissions = [Permission::read(Role::any()), Permission::write(Role::any()), Permission::update(Role::any())]; + + // Extra attributes should fail + $docs = [ + new Document(['$id' => 'doc11', 'key' => 'doc1', 'title' => 'doc1', '$permissions' => $permissions]), + new Document(['$id' => 'doc21', 'key' => 'doc2', 'moviename' => 'doc2', 'moviedescription' => 'test', '$permissions' => $permissions]), + new Document(['$id' => 'doc31', 'key' => 'doc3', '$permissions' => $permissions]), + ]; + + $this->expectException(StructureException::class); + $database->createDocuments($colName, $docs); + + $database->deleteCollection($colName); + } + + public function testSchemalessSelectionOnUnknownAttributes(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $colName = uniqid("schemaless"); + $database->createCollection($colName); + $permissions = [Permission::read(Role::any()), Permission::write(Role::any()), Permission::update(Role::any())]; + $docs = [ + new Document(['$id' => 'doc1', '$permissions' => $permissions, 'freeA' => 'doc1']), + new Document(['$id' => 'doc2', '$permissions' => $permissions, 'freeB' => 'test']), + new Document(['$id' => 'doc3', '$permissions' => $permissions]), + ]; + $this->assertEquals(3, $database->createDocuments($colName, $docs)); + + $docA = $database->getDocument($colName, 'doc1', [Query::select(['freeA'])]); + $this->assertEquals('doc1', $docA->getAttribute('freeA')); + + $docC = $database->getDocument($colName, 'doc1', [Query::select(['freeC'])]); + $this->assertNull($docC->getAttribute('freeC')); + + $docs = $database->find($colName, [Query::equal('$id', ['doc1','doc2']),Query::select(['freeC'])]); + foreach ($docs as $doc) { + $this->assertNull($doc->getAttribute('freeC')); + // since not selected + $this->assertNull($doc->getAttribute('freeA')); + $this->assertNull($doc->getAttribute('freeB')); + } + + $docA = $database->find($colName, [ + Query::equal('$id', ['doc1']), + Query::select(['freeA']) + ]); + $this->assertEquals('doc1', $docA[0]->getAttribute('freeA')); + + $docC = $database->find($colName, [ + Query::equal('$id', ['doc1']), + Query::select(['freeC']) + ]); + $this->assertNull($docC[0]->getAttribute('freeC')); + } + + public function testSchemalessIncrement(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $colName = uniqid("schemaless_increment"); + $database->createCollection($colName); + + $permissions = [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ]; + + $docs = [ + new Document(['$id' => 'doc1', '$permissions' => $permissions, 'counter' => '10', 'score' => 5.5]), + new Document(['$id' => 'doc2', '$permissions' => $permissions, 'counter' => 20, 'points' => 100]), + new Document(['$id' => 'doc3', '$permissions' => $permissions, 'value' => 0]), + ]; + $this->assertEquals(3, $database->createDocuments($colName, $docs)); + + $doc1 = $database->increaseDocumentAttribute($colName, 'doc1', 'counter', 5); + $this->assertEquals(15, $doc1->getAttribute('counter')); + $this->assertEquals(5.5, $doc1->getAttribute('score')); + + $doc1 = $database->increaseDocumentAttribute($colName, 'doc1', 'score', 2.3); + $this->assertEquals(7.8, $doc1->getAttribute('score')); + + $doc2 = $database->increaseDocumentAttribute($colName, 'doc2', 'points', 50); + $this->assertEquals(150, $doc2->getAttribute('points')); + + $doc3 = $database->increaseDocumentAttribute($colName, 'doc3', 'newCounter', 1); + $this->assertEquals(1, $doc3->getAttribute('newCounter')); + $this->assertEquals(0, $doc3->getAttribute('value')); + + try { + $database->increaseDocumentAttribute($colName, 'doc1', 'counter', 10, 20); + $this->assertEquals(20, $database->getDocument($colName, 'doc1')->getAttribute('counter')); + } catch (\Exception $e) { + $this->assertInstanceOf(LimitException::class, $e); + } + + $allDocs = $database->find($colName); + $this->assertCount(3, $allDocs); + + $database->deleteCollection($colName); + } + + public function testSchemalessDecrement(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $colName = uniqid("schemaless_decrement"); + $database->createCollection($colName); + + $permissions = [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ]; + + $docs = [ + new Document(['$id' => 'doc1', '$permissions' => $permissions, 'counter' => 100, 'balance' => 250.75]), + new Document(['$id' => 'doc2', '$permissions' => $permissions, 'score' => 50, 'extraData' => 'preserved']), + ]; + $this->assertEquals(2, $database->createDocuments($colName, $docs)); + + $doc1 = $database->decreaseDocumentAttribute($colName, 'doc1', 'counter', 25); + $this->assertEquals(75, $doc1->getAttribute('counter')); + $this->assertEquals(250.75, $doc1->getAttribute('balance')); + + $doc1 = $database->decreaseDocumentAttribute($colName, 'doc1', 'balance', 50.25); + $this->assertEquals(200.5, $doc1->getAttribute('balance')); + + $doc2 = $database->decreaseDocumentAttribute($colName, 'doc2', 'score', 15); + $this->assertEquals(35, $doc2->getAttribute('score')); + $this->assertEquals('preserved', $doc2->getAttribute('extraData')); + + try { + $database->decreaseDocumentAttribute($colName, 'doc2', 'score', 40, 0); + $this->fail('Expected LimitException not thrown'); + } catch (\Exception $e) { + $this->assertInstanceOf(LimitException::class, $e); + } + + $doc2 = $database->decreaseDocumentAttribute($colName, 'doc2', 'score', 50); + $this->assertEquals(-15, $doc2->getAttribute('score')); + + $retrievedDoc1 = $database->getDocument($colName, 'doc1'); + $this->assertEquals(75, $retrievedDoc1->getAttribute('counter')); + $this->assertEquals(200.5, $retrievedDoc1->getAttribute('balance')); + + $database->deleteCollection($colName); + } + + public function testSchemalessUpdateDocumentWithQuery(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $colName = uniqid("schemaless_update"); + $database->createCollection($colName); + + $permissions = [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ]; + + $docs = [ + new Document(['$id' => 'doc1', '$permissions' => $permissions, 'type' => 'user', 'status' => 'active', 'score' => 100]), + new Document(['$id' => 'doc2', '$permissions' => $permissions, 'type' => 'admin', 'status' => 'active', 'level' => 5]), + new Document(['$id' => 'doc3', '$permissions' => $permissions, 'type' => 'user', 'status' => 'inactive', 'score' => 50]), + new Document(['$id' => 'doc4', '$permissions' => $permissions, 'type' => 'user', 'status' => 'pending', 'newField' => 'test']), + ]; + $this->assertEquals(4, $database->createDocuments($colName, $docs)); + + $updatedDoc = $database->updateDocument($colName, 'doc1', new Document([ + 'status' => 'updated', + 'lastModified' => '2023-01-01', + 'newAttribute' => 'added' + ])); + + $this->assertEquals('updated', $updatedDoc->getAttribute('status')); + $this->assertEquals('2023-01-01', $updatedDoc->getAttribute('lastModified')); + $this->assertEquals('added', $updatedDoc->getAttribute('newAttribute')); + $this->assertEquals('user', $updatedDoc->getAttribute('type')); // Existing attributes preserved + $this->assertEquals(100, $updatedDoc->getAttribute('score')); + + $retrievedDoc = $database->getDocument($colName, 'doc1'); + $this->assertEquals('updated', $retrievedDoc->getAttribute('status')); + $this->assertEquals('added', $retrievedDoc->getAttribute('newAttribute')); + + $updatedDoc2 = $database->updateDocument($colName, 'doc2', new Document([ + 'customField1' => 'value1', + 'customField2' => 42, + 'customField3' => ['array', 'of', 'values'] + ])); + + $this->assertEquals('value1', $updatedDoc2->getAttribute('customField1')); + $this->assertEquals(42, $updatedDoc2->getAttribute('customField2')); + $this->assertEquals(['array', 'of', 'values'], $updatedDoc2->getAttribute('customField3')); + $this->assertEquals('admin', $updatedDoc2->getAttribute('type')); // Original attributes preserved + + $database->deleteCollection($colName); + } + + public function testSchemalessDeleteDocumentWithQuery(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $colName = uniqid("schemaless_delete"); + $database->createCollection($colName); + + $permissions = [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ]; + + $docs = [ + new Document(['$id' => 'doc1', '$permissions' => $permissions, 'category' => 'temp', 'priority' => 1]), + new Document(['$id' => 'doc2', '$permissions' => $permissions, 'category' => 'permanent', 'priority' => 5]), + new Document(['$id' => 'doc3', '$permissions' => $permissions, 'category' => 'temp', 'priority' => 3]), + ]; + $this->assertEquals(3, $database->createDocuments($colName, $docs)); + + $result = $database->deleteDocument($colName, 'doc1'); + $this->assertTrue($result); + + $deletedDoc = $database->getDocument($colName, 'doc1'); + $this->assertTrue($deletedDoc->isEmpty()); + + $remainingDocs = $database->find($colName); + $this->assertCount(2, $remainingDocs); + + $tempDocs = $database->find($colName, [Query::equal('category', ['temp'])]); + $this->assertCount(1, $tempDocs); + $this->assertEquals('doc3', $tempDocs[0]->getId()); + + $database->deleteCollection($colName); + } + + public function testSchemalessUpdateDocumentsWithQuery(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + if (!$database->getAdapter()->getSupportForBatchOperations()) { + $this->expectNotToPerformAssertions(); + return; + } + + $colName = uniqid("schemaless_bulk_update"); + $database->createCollection($colName); + + $permissions = [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ]; + + $docs = []; + for ($i = 1; $i <= 10; $i++) { + $docs[] = new Document([ + '$id' => "doc{$i}", + '$permissions' => $permissions, + 'type' => $i <= 5 ? 'typeA' : 'typeB', + 'status' => 'pending', + 'score' => $i * 10, + 'customField' => "value{$i}" + ]); + } + $this->assertEquals(10, $database->createDocuments($colName, $docs)); + + $updatedCount = $database->updateDocuments($colName, new Document([ + 'status' => 'processed', + 'processedAt' => '2023-01-01', + 'newBulkField' => 'bulk_value' + ]), [Query::equal('type', ['typeA'])]); + + $this->assertEquals(5, $updatedCount); + + $processedDocs = $database->find($colName, [Query::equal('status', ['processed'])]); + $this->assertCount(5, $processedDocs); + + foreach ($processedDocs as $doc) { + $this->assertEquals('typeA', $doc->getAttribute('type')); + $this->assertEquals('processed', $doc->getAttribute('status')); + $this->assertEquals('2023-01-01', $doc->getAttribute('processedAt')); + $this->assertEquals('bulk_value', $doc->getAttribute('newBulkField')); + $this->assertNotNull($doc->getAttribute('score')); + $this->assertNotNull($doc->getAttribute('customField')); + } + + $pendingDocs = $database->find($colName, [Query::equal('status', ['pending'])]); + $this->assertCount(5, $pendingDocs); + + foreach ($pendingDocs as $doc) { + $this->assertEquals('typeB', $doc->getAttribute('type')); + $this->assertEquals('pending', $doc->getAttribute('status')); + $this->assertNull($doc->getAttribute('processedAt')); + $this->assertNull($doc->getAttribute('newBulkField')); + } + + $highScoreCount = $database->updateDocuments($colName, new Document([ + 'tier' => 'premium' + ]), [Query::greaterThan('score', 70)]); + + $this->assertEquals(3, $highScoreCount); // docs 8, 9, 10 + + $premiumDocs = $database->find($colName, [Query::equal('tier', ['premium'])]); + $this->assertCount(3, $premiumDocs); + + $allUpdateCount = $database->updateDocuments($colName, new Document([ + 'globalFlag' => true, + 'lastUpdate' => '2023-12-31' + ])); + + $this->assertEquals(10, $allUpdateCount); + + $allDocs = $database->find($colName); + $this->assertCount(10, $allDocs); + + foreach ($allDocs as $doc) { + $this->assertTrue($doc->getAttribute('globalFlag')); + $this->assertEquals('2023-12-31', $doc->getAttribute('lastUpdate')); + } + + $database->deleteCollection($colName); + } + + public function testSchemalessDeleteDocumentsWithQuery(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + if (!$database->getAdapter()->getSupportForBatchOperations()) { + $this->expectNotToPerformAssertions(); + return; + } + + $colName = uniqid("schemaless_bulk_delete"); + $database->createCollection($colName); + + $permissions = [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ]; + + $docs = []; + for ($i = 1; $i <= 15; $i++) { + $docs[] = new Document([ + '$id' => "doc{$i}", + '$permissions' => $permissions, + 'category' => $i <= 5 ? 'temp' : ($i <= 10 ? 'archive' : 'active'), + 'priority' => $i % 3, // 0, 1, or 2 + 'score' => $i * 5, + 'tags' => ["tag{$i}", 'common'], + 'metadata' => ['created' => "2023-01-{$i}"] + ]); + } + $this->assertEquals(15, $database->createDocuments($colName, $docs)); + + $deletedCount = $database->deleteDocuments($colName, [Query::equal('category', ['temp'])]); + $this->assertEquals(5, $deletedCount); + + $remainingDocs = $database->find($colName); + $this->assertCount(10, $remainingDocs); + + $tempDocs = $database->find($colName, [Query::equal('category', ['temp'])]); + $this->assertCount(0, $tempDocs); + + $highScoreDeleted = $database->deleteDocuments($colName, [Query::greaterThan('score', 50)]); + $this->assertEquals(5, $highScoreDeleted); // docs 11-15 + + $remainingAfterScore = $database->find($colName); + $this->assertCount(5, $remainingAfterScore); // docs 6-10 remain + + foreach ($remainingAfterScore as $doc) { + $this->assertLessThanOrEqual(50, $doc->getAttribute('score')); + $this->assertEquals('archive', $doc->getAttribute('category')); + } + + $multiConditionDeleted = $database->deleteDocuments($colName, [ + Query::equal('category', ['archive']), + Query::equal('priority', [1]) + ]); + $this->assertEquals(2, $multiConditionDeleted); // docs 7 and 10 + + $finalRemaining = $database->find($colName); + $this->assertCount(3, $finalRemaining); // docs 6, 8, 9 + + foreach ($finalRemaining as $doc) { + $this->assertEquals('archive', $doc->getAttribute('category')); + $this->assertNotEquals(1, $doc->getAttribute('priority')); + } + + $allDeleted = $database->deleteDocuments($colName); + $this->assertEquals(3, $allDeleted); + + $emptyResult = $database->find($colName); + $this->assertCount(0, $emptyResult); + + $database->deleteCollection($colName); + } + + public function testSchemalessOperationsWithCallback(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + if (!$database->getAdapter()->getSupportForBatchOperations()) { + $this->expectNotToPerformAssertions(); + return; + } + + $colName = uniqid("schemaless_callbacks"); + $database->createCollection($colName); + + $permissions = [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ]; + + $docs = []; + for ($i = 1; $i <= 8; $i++) { + $docs[] = new Document([ + '$id' => "doc{$i}", + '$permissions' => $permissions, + 'group' => $i <= 4 ? 'A' : 'B', + 'value' => $i * 10, + 'customData' => "data{$i}" + ]); + } + $this->assertEquals(8, $database->createDocuments($colName, $docs)); + + $updateResults = []; + $updateCount = $database->updateDocuments( + $colName, + new Document(['processed' => true, 'timestamp' => '2023-01-01']), + [Query::equal('group', ['A'])], + onNext: function ($doc) use (&$updateResults) { + $updateResults[] = $doc->getId(); + } + ); + + $this->assertEquals(4, $updateCount); + $this->assertCount(4, $updateResults); + $this->assertContains('doc1', $updateResults); + $this->assertContains('doc2', $updateResults); + $this->assertContains('doc3', $updateResults); + $this->assertContains('doc4', $updateResults); + + $processedDocs = $database->find($colName, [Query::equal('processed', [true])]); + $this->assertCount(4, $processedDocs); + + $deleteResults = []; + $deleteCount = $database->deleteDocuments( + $colName, + [Query::greaterThan('value', 50)], + onNext: function ($doc) use (&$deleteResults) { + $deleteResults[] = [ + 'id' => $doc->getId(), + 'value' => $doc->getAttribute('value'), + 'customData' => $doc->getAttribute('customData') + ]; + } + ); + + $this->assertEquals(3, $deleteCount); // docs 6, 7, 8 + $this->assertCount(3, $deleteResults); + + foreach ($deleteResults as $result) { + $this->assertGreaterThan(50, $result['value']); + $this->assertStringStartsWith('data', $result['customData']); + } + + $remainingDocs = $database->find($colName); + $this->assertCount(5, $remainingDocs); // docs 1-5 + + foreach ($remainingDocs as $doc) { + $this->assertLessThanOrEqual(50, $doc->getAttribute('value')); + } + + $database->deleteCollection($colName); + } + + public function testSchemalessIndexCreateListDelete(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + // Schemaless adapter still supports defining attributes/indexes metadata + $col = uniqid('sl_idx'); + $database->createCollection($col); + + $database->createDocument($col, new Document([ + '$id' => 'a', + '$permissions' => [Permission::read(Role::any())], + 'title' => 't1', + 'rank' => 1, + ])); + $database->createDocument($col, new Document([ + '$id' => 'b', + '$permissions' => [Permission::read(Role::any())], + 'title' => 't2', + 'rank' => 2, + ])); + + $this->assertTrue($database->createIndex($col, 'idx_title_unique', Database::INDEX_UNIQUE, ['title'], [128], [Database::ORDER_ASC])); + $this->assertTrue($database->createIndex($col, 'idx_rank_key', Database::INDEX_KEY, ['rank'], [0], [Database::ORDER_ASC])); + + $collection = $database->getCollection($col); + $indexes = $collection->getAttribute('indexes'); + $this->assertCount(2, $indexes); + $ids = array_map(fn ($i) => $i['$id'], $indexes); + $this->assertContains('idx_rank_key', $ids); + $this->assertContains('idx_title_unique', $ids); + + $this->assertTrue($database->deleteIndex($col, 'idx_rank_key')); + $collection = $database->getCollection($col); + $this->assertCount(1, $collection->getAttribute('indexes')); + $this->assertEquals('idx_title_unique', $collection->getAttribute('indexes')[0]['$id']); + + $this->assertTrue($database->deleteIndex($col, 'idx_title_unique')); + $database->deleteCollection($col); + } + + public function testSchemalessIndexDuplicatePrevention(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $col = uniqid('sl_idx_dup'); + $database->createCollection($col); + + $database->createDocument($col, new Document([ + '$id' => 'a', + '$permissions' => [Permission::read(Role::any())], + 'name' => 'x' + ])); + + $this->assertTrue($database->createIndex($col, 'duplicate', Database::INDEX_KEY, ['name'], [0], [Database::ORDER_ASC])); + + try { + $database->createIndex($col, 'duplicate', Database::INDEX_KEY, ['name'], [0], [Database::ORDER_ASC]); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(DuplicateException::class, $e); + } + + $database->deleteCollection($col); + } + + public function testSchemalessPermissions(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $col = uniqid('sl_perms'); + $database->createCollection($col); + + // Create with permissive read only + $doc = $database->createDocument($col, new Document([ + '$id' => 'd1', + '$permissions' => [ + Permission::read(Role::any()) + ], + 'field' => 'value' + ])); + + $this->assertFalse($doc->isEmpty()); + + // Without roles, cannot read + Authorization::cleanRoles(); + $empty = $database->getDocument($col, 'd1'); + $this->assertTrue($empty->isEmpty()); + + // With any role, can read + Authorization::setRole(Role::any()->toString()); + $fetched = $database->getDocument($col, 'd1'); + $this->assertEquals('value', $fetched->getAttribute('field')); + + // Attempt update without update permission + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); + try { + $database->updateDocument($col, 'd1', new Document(['field' => 'updated'])); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(AuthorizationException::class, $e); + } + + // Grant update permission and update + Authorization::skip(function () use ($database, $col) { + $database->updateDocument($col, 'd1', new Document([ + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + ] + ])); + }); + + $updated = $database->updateDocument($col, 'd1', new Document(['field' => 'updated'])); + $this->assertEquals('updated', $updated->getAttribute('field')); + + // Creating without any roles should fail + Authorization::cleanRoles(); + try { + $database->createDocument($col, new Document([ + 'field' => 'x' + ])); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(AuthorizationException::class, $e); + } + + $database->deleteCollection($col); + Authorization::cleanRoles(); + } + + public function testSchemalessInternalAttributes(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $col = uniqid('sl_internal_full'); + $database->createCollection($col); + + Authorization::setRole(Role::any()->toString()); + + $doc = $database->createDocument($col, new Document([ + '$id' => 'i1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'alpha', + ])); + + $this->assertEquals('i1', $doc->getId()); + $this->assertEquals($col, $doc->getCollection()); + $this->assertNotEmpty($doc->getSequence()); + $this->assertNotEmpty($doc->getAttribute('$createdAt')); + $this->assertNotEmpty($doc->getAttribute('$updatedAt')); + $perms = $doc->getPermissions(); + $this->assertGreaterThanOrEqual(1, count($perms)); + $this->assertContains(Permission::read(Role::any()), $perms); + $this->assertContains(Permission::update(Role::any()), $perms); + $this->assertContains(Permission::delete(Role::any()), $perms); + + $selected = $database->getDocument($col, 'i1', [ + Query::select(['name', '$id', '$sequence', '$collection', '$createdAt', '$updatedAt', '$permissions']) + ]); + $this->assertEquals('alpha', $selected->getAttribute('name')); + $this->assertArrayHasKey('$id', $selected); + $this->assertArrayHasKey('$sequence', $selected); + $this->assertArrayHasKey('$collection', $selected); + $this->assertArrayHasKey('$createdAt', $selected); + $this->assertArrayHasKey('$updatedAt', $selected); + $this->assertArrayHasKey('$permissions', $selected); + + $found = $database->find($col, [ + Query::equal('$id', ['i1']), + Query::select(['$id', '$sequence', '$collection', '$createdAt', '$updatedAt', '$permissions']) + ]); + $this->assertCount(1, $found); + $this->assertArrayHasKey('$id', $found[0]); + $this->assertArrayHasKey('$sequence', $found[0]); + $this->assertArrayHasKey('$collection', $found[0]); + $this->assertArrayHasKey('$createdAt', $found[0]); + $this->assertArrayHasKey('$updatedAt', $found[0]); + $this->assertArrayHasKey('$permissions', $found[0]); + + $seq = $doc->getSequence(); + $bySeq = $database->find($col, [Query::equal('$sequence', [$seq])]); + $this->assertCount(1, $bySeq); + $this->assertEquals('i1', $bySeq[0]->getId()); + + $createdAtBefore = $doc->getAttribute('$createdAt'); + $updatedAtBefore = $doc->getAttribute('$updatedAt'); + $updated = $database->updateDocument($col, 'i1', new Document(['name' => 'beta'])); + $this->assertEquals('beta', $updated->getAttribute('name')); + $this->assertEquals($createdAtBefore, $updated->getAttribute('$createdAt')); + $this->assertNotEquals($updatedAtBefore, $updated->getAttribute('$updatedAt')); + + $changed = $database->updateDocument($col, 'i1', new Document(['$id' => 'i1-new'])); + $this->assertEquals('i1-new', $changed->getId()); + $refetched = $database->getDocument($col, 'i1-new'); + $this->assertEquals('i1-new', $refetched->getId()); + + try { + $database->updateDocument($col, 'i1-new', new Document(['$permissions' => 'invalid'])); + $this->fail('Failed to throw exception'); + } catch (Throwable $e) { + $this->assertTrue($e instanceof StructureException); + } + + $database->setPreserveDates(true); + $customCreated = '2000-01-01T00:00:00.000+00:00'; + $customUpdated = '2000-01-02T00:00:00.000+00:00'; + $d2 = $database->createDocument($col, new Document([ + '$id' => 'i2', + '$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())], + '$createdAt' => $customCreated, + '$updatedAt' => $customUpdated, + 'v' => 1 + ])); + $this->assertEquals($customCreated, $d2->getAttribute('$createdAt')); + $this->assertEquals($customUpdated, $d2->getAttribute('$updatedAt')); + + $newUpdated = '2000-01-03T00:00:00.000+00:00'; + $d2u = $database->updateDocument($col, 'i2', new Document([ + 'v' => 2, + '$updatedAt' => $newUpdated + ])); + $this->assertEquals($customCreated, $d2u->getAttribute('$createdAt')); + $this->assertEquals($newUpdated, $d2u->getAttribute('$updatedAt')); + $database->setPreserveDates(false); + + $database->deleteCollection($col); + Authorization::cleanRoles(); + } +} From b0855b617a743229907d8d50cd04cd02f4464ad8 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Sun, 28 Sep 2025 12:26:50 +0530 Subject: [PATCH 05/15] synced with feat-mongo-tmp branch --- composer.lock | 12 ++++++------ tests/unit/Validator/IndexTest.php | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index 07fdebb32..af5eb8778 100644 --- a/composer.lock +++ b/composer.lock @@ -1383,16 +1383,16 @@ }, { "name": "symfony/http-client", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019" + "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019", - "reference": "333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019", + "url": "https://api.github.com/repos/symfony/http-client/zipball/4b62871a01c49457cf2a8e560af7ee8a94b87a62", + "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62", "shasum": "" }, "require": { @@ -1459,7 +1459,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.3" + "source": "https://github.com/symfony/http-client/tree/v7.3.4" }, "funding": [ { @@ -1479,7 +1479,7 @@ "type": "tidelift" } ], - "time": "2025-08-27T07:45:05+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "symfony/http-client-contracts", diff --git a/tests/unit/Validator/IndexTest.php b/tests/unit/Validator/IndexTest.php index 55b7aaecc..62f42ba07 100644 --- a/tests/unit/Validator/IndexTest.php +++ b/tests/unit/Validator/IndexTest.php @@ -382,11 +382,11 @@ public function testIndexWithNoAttributeSupport(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768); + $validator = new Index(attributes:$collection->getAttribute('attributes'), indexes:$collection->getAttribute('indexes'), maxLength:768); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); - $validator = new Index($collection->getAttribute('attributes'), 768, supportForAttributes:false); + $validator = new Index(attributes: $collection->getAttribute('attributes'), indexes:$collection->getAttribute('indexes'), maxLength: 768, supportForAttributes:false); $index = $collection->getAttribute('indexes')[0]; $this->assertTrue($validator->isValid($index)); } From 0c12424756065340bcc8138317d87d1e352890b7 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 29 Sep 2025 16:38:05 +0530 Subject: [PATCH 06/15] updated tests --- src/Database/Adapter/Mongo.php | 32 ++++--- tests/e2e/Adapter/Scopes/AttributeTests.php | 2 +- tests/e2e/Adapter/Scopes/IndexTests.php | 89 +++++++++++--------- tests/e2e/Adapter/Scopes/SchemalessTests.php | 2 +- 4 files changed, 71 insertions(+), 54 deletions(-) diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 78210502a..18a9665ef 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -14,6 +14,7 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Exception\Timeout; +use Utopia\Database\Exception\Type as TypeException; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Mongo\Client; @@ -126,8 +127,8 @@ public function withTransaction(callable $callback): mixed public function startTransaction(): bool { - // If the database is not a replica set, we can't use transactions - if (!$this->client->isReplicaSet()) { + // If the database is not a replica set, we can't use transactions + if (!$this->client->isReplicaSet()) { return false; } @@ -1531,15 +1532,19 @@ public function increaseDocumentAttribute(string $collection, string $id, string } $options = $this->getTransactionOptions(); - $this->client->update( - $this->getNamespace() . '_' . $this->filter($collection), - $filters, - [ - '$inc' => [$attribute => $value], - '$set' => ['_updatedAt' => $this->toMongoDatetime($updatedAt)], - ], - options: $options - ); + try { + $this->client->update( + $this->getNamespace() . '_' . $this->filter($collection), + $filters, + [ + '$inc' => [$attribute => $value], + '$set' => ['_updatedAt' => $this->toMongoDatetime($updatedAt)], + ], + options: $options + ); + } catch (MongoException $e) { + throw $this->processException($e); + } return true; } @@ -2823,6 +2828,11 @@ protected function processException(Exception $e): \Exception return new Duplicate('Index already exists', $e->getCode(), $e); } + // Invalid operation(MongoDB error code 14) + if ($e->getCode() === 14) { + return new TypeException('Invalid operation', $e->getCode(), $e); + } + return $e; } diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index 53bdd54e7..8add6b1d8 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -1523,7 +1523,7 @@ public function testArrayAttribute(): void )); if ($database->getAdapter()->getSupportForIndexArray()) { - if ($database->getAdapter()->getMaxIndexLength() > 0) { + if ($database->getAdapter()->getSupportForAttributes() && $database->getAdapter()->getMaxIndexLength() > 0) { // If getMaxIndexLength() > 0 We clear length for array attributes $database->createIndex($collection, 'indx1', Database::INDEX_KEY, ['long_size'], [], []); $database->deleteIndex($collection, 'indx1'); diff --git a/tests/e2e/Adapter/Scopes/IndexTests.php b/tests/e2e/Adapter/Scopes/IndexTests.php index ac11e11cd..c8f9ea360 100644 --- a/tests/e2e/Adapter/Scopes/IndexTests.php +++ b/tests/e2e/Adapter/Scopes/IndexTests.php @@ -175,19 +175,22 @@ public function testIndexValidation(): void $database->getAdapter()->getSupportForMultipleFulltextIndexes(), $database->getAdapter()->getSupportForIdenticalIndexes() ); + if ($database->getAdapter()->getSupportForAttributes()) { + $errorMessage = 'Index length 701 is larger than the size for title1: 700"'; + $this->assertFalse($validator->isValid($indexes[0])); + $this->assertEquals($errorMessage, $validator->getDescription()); + } - $errorMessage = 'Index length 701 is larger than the size for title1: 700"'; - $this->assertFalse($validator->isValid($indexes[0])); - $this->assertEquals($errorMessage, $validator->getDescription()); - - try { - $database->createCollection($collection->getId(), $attributes, $indexes, [ - Permission::read(Role::any()), - Permission::create(Role::any()), - ]); - $this->fail('Failed to throw exception'); - } catch (Exception $e) { - $this->assertEquals($errorMessage, $e->getMessage()); + if ($database->getAdapter()->getSupportForAttributes()) { + try { + $database->createCollection($collection->getId(), $attributes, $indexes, [ + Permission::read(Role::any()), + Permission::create(Role::any()), + ]); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertEquals($errorMessage, $e->getMessage()); + } } $indexes = [ @@ -202,7 +205,7 @@ public function testIndexValidation(): void $collection->setAttribute('indexes', $indexes); - if ($database->getAdapter()->getMaxIndexLength() > 0) { + if ($database->getAdapter()->getSupportForAttributes() && $database->getAdapter()->getMaxIndexLength() > 0) { $errorMessage = 'Index length is longer than the maximum: ' . $database->getAdapter()->getMaxIndexLength(); $this->assertFalse($validator->isValid($indexes[0])); $this->assertEquals($errorMessage, $validator->getDescription()); @@ -285,37 +288,37 @@ public function testIndexValidation(): void 'orders' => [], ]), ]; + if ($database->getAdapter()->getSupportForAttributes()) { + $errorMessage = 'Negative index length provided for title1'; + $this->assertFalse($validator->isValid($indexes[0])); + $this->assertEquals($errorMessage, $validator->getDescription()); - $errorMessage = 'Negative index length provided for title1'; - $this->assertFalse($validator->isValid($indexes[0])); - $this->assertEquals($errorMessage, $validator->getDescription()); - - try { - $database->createCollection(ID::unique(), $attributes, $indexes); - $this->fail('Failed to throw exception'); - } catch (Exception $e) { - $this->assertEquals($errorMessage, $e->getMessage()); - } - - $indexes = [ - new Document([ - '$id' => ID::custom('index_extra_lengths'), - 'type' => Database::INDEX_KEY, - 'attributes' => ['title1', 'title2'], - 'lengths' => [100, 100, 100], - 'orders' => [], - ]), - ]; + try { + $database->createCollection(ID::unique(), $attributes, $indexes); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertEquals($errorMessage, $e->getMessage()); + } - $errorMessage = 'Invalid index lengths. Count of lengths must be equal or less than the number of attributes.'; - $this->assertFalse($validator->isValid($indexes[0])); - $this->assertEquals($errorMessage, $validator->getDescription()); + $indexes = [ + new Document([ + '$id' => ID::custom('index_extra_lengths'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['title1', 'title2'], + 'lengths' => [100, 100, 100], + 'orders' => [], + ]), + ]; + $errorMessage = 'Invalid index lengths. Count of lengths must be equal or less than the number of attributes.'; + $this->assertFalse($validator->isValid($indexes[0])); + $this->assertEquals($errorMessage, $validator->getDescription()); - try { - $database->createCollection(ID::unique(), $attributes, $indexes); - $this->fail('Failed to throw exception'); - } catch (Exception $e) { - $this->assertEquals($errorMessage, $e->getMessage()); + try { + $database->createCollection(ID::unique(), $attributes, $indexes); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertEquals($errorMessage, $e->getMessage()); + } } } @@ -323,6 +326,10 @@ public function testIndexLengthZero(): void { /** @var Database $database */ $database = static::getDatabase(); + if (!$database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } $database->createCollection(__FUNCTION__); diff --git a/tests/e2e/Adapter/Scopes/SchemalessTests.php b/tests/e2e/Adapter/Scopes/SchemalessTests.php index dd40fd21e..73b6b3ba5 100644 --- a/tests/e2e/Adapter/Scopes/SchemalessTests.php +++ b/tests/e2e/Adapter/Scopes/SchemalessTests.php @@ -253,7 +253,7 @@ public function testSchemalessIncrement(): void ]; $docs = [ - new Document(['$id' => 'doc1', '$permissions' => $permissions, 'counter' => '10', 'score' => 5.5]), + new Document(['$id' => 'doc1', '$permissions' => $permissions, 'counter' => 10, 'score' => 5.5]), new Document(['$id' => 'doc2', '$permissions' => $permissions, 'counter' => 20, 'points' => 100]), new Document(['$id' => 'doc3', '$permissions' => $permissions, 'value' => 0]), ]; From 84cbadf36bb1dfad1cd60fbca06275cd78aa35cc Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 29 Sep 2025 16:42:53 +0530 Subject: [PATCH 07/15] added document mutation fix for the creaetDocuments and tests for schemaless dates operation --- src/Database/Database.php | 12 +- tests/e2e/Adapter/Scopes/SchemalessTests.php | 224 +++++++++++++++++++ 2 files changed, 230 insertions(+), 6 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 01e19047c..4c776d97c 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4049,17 +4049,17 @@ public function createDocuments( $batch = $this->adapter->getSequences($collection->getId(), $batch); - foreach ($batch as $document) { - $document = $this->adapter->castingAfter($collection, $document); + foreach ($batch as $doc) { + $doc = $this->adapter->castingAfter($collection, $doc); if ($this->resolveRelationships) { - $document = $this->silent(fn () => $this->populateDocumentRelationships($collection, $document)); + $doc = $this->silent(fn () => $this->populateDocumentRelationships($collection, $doc)); } - $document = $this->casting($collection, $document); - $document = $this->decode($collection, $document); + $doc = $this->casting($collection, $doc); + $doc = $this->decode($collection, $doc); try { - $onNext && $onNext($document); + $onNext && $onNext($doc); } catch (\Throwable $e) { $onError ? $onError($e) : throw $e; } diff --git a/tests/e2e/Adapter/Scopes/SchemalessTests.php b/tests/e2e/Adapter/Scopes/SchemalessTests.php index 73b6b3ba5..111966783 100644 --- a/tests/e2e/Adapter/Scopes/SchemalessTests.php +++ b/tests/e2e/Adapter/Scopes/SchemalessTests.php @@ -964,4 +964,228 @@ public function testSchemalessInternalAttributes(): void $database->deleteCollection($col); Authorization::cleanRoles(); } + + public function testSchemalessDates(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $col = uniqid('sl_dates'); + $database->createCollection($col); + + $permissions = [ + Permission::read(Role::any()), + Permission::write(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ]; + + // Seed deterministic date strings + $createdAt1 = '2000-01-01T10:00:00.000+00:00'; + $updatedAt1 = '2000-01-02T11:11:11.000+00:00'; + $curDate1 = '2000-01-05T05:05:05.000+00:00'; + + // createDocument with preserved dates + $doc1 = $database->withPreserveDates(function () use ($database, $col, $permissions, $createdAt1, $updatedAt1, $curDate1) { + return $database->createDocument($col, new Document([ + '$id' => 'd1', + '$permissions' => $permissions, + '$createdAt' => $createdAt1, + '$updatedAt' => $updatedAt1, + 'curDate' => $curDate1, + 'counter' => 0, + ])); + }); + + $this->assertEquals('d1', $doc1->getId()); + $this->assertTrue(is_string($doc1->getAttribute('curDate'))); + $this->assertEquals($curDate1, $doc1->getAttribute('curDate')); + $this->assertTrue(is_string($doc1->getAttribute('$createdAt'))); + $this->assertTrue(is_string($doc1->getAttribute('$updatedAt'))); + $this->assertEquals($createdAt1, $doc1->getAttribute('$createdAt')); + $this->assertEquals($updatedAt1, $doc1->getAttribute('$updatedAt')); + + $fetched1 = $database->getDocument($col, 'd1'); + $this->assertEquals($curDate1, $fetched1->getAttribute('curDate')); + $this->assertTrue(is_string($fetched1->getAttribute('curDate'))); + $this->assertTrue(is_string($fetched1->getAttribute('$createdAt'))); + $this->assertTrue(is_string($fetched1->getAttribute('$updatedAt'))); + $this->assertEquals($createdAt1, $fetched1->getAttribute('$createdAt')); + $this->assertEquals($updatedAt1, $fetched1->getAttribute('$updatedAt')); + + // createDocuments with preserved dates + $createdAt2 = '2001-02-03T04:05:06.000+00:00'; + $updatedAt2 = '2001-02-04T04:05:07.000+00:00'; + $curDate2 = '2001-02-05T06:07:08.000+00:00'; + + $createdAt3 = '2002-03-04T05:06:07.000+00:00'; + $updatedAt3 = '2002-03-05T05:06:08.000+00:00'; + $curDate3 = '2002-03-06T07:08:09.000+00:00'; + + $countCreated = $database->withPreserveDates(function () use ($database, $col, $permissions, $createdAt2, $updatedAt2, $curDate2, $createdAt3, $updatedAt3, $curDate3) { + return $database->createDocuments($col, [ + new Document([ + '$id' => 'd2', + '$permissions' => $permissions, + '$createdAt' => $createdAt2, + '$updatedAt' => $updatedAt2, + 'curDate' => $curDate2, + ]), + new Document([ + '$id' => 'd3', + '$permissions' => $permissions, + '$createdAt' => $createdAt3, + '$updatedAt' => $updatedAt3, + 'curDate' => $curDate3, + ]), + ]); + }); + $this->assertEquals(2, $countCreated); + + $fetched2 = $database->getDocument($col, 'd2'); + $this->assertEquals($curDate2, $fetched2->getAttribute('curDate')); + $this->assertEquals($createdAt2, $fetched2->getAttribute('$createdAt')); + $this->assertEquals($updatedAt2, $fetched2->getAttribute('$updatedAt')); + + $fetched3 = $database->getDocument($col, 'd3'); + $this->assertEquals($curDate3, $fetched3->getAttribute('curDate')); + $this->assertEquals($createdAt3, $fetched3->getAttribute('$createdAt')); + $this->assertEquals($updatedAt3, $fetched3->getAttribute('$updatedAt')); + + // updateDocument with preserved $updatedAt and custom date field + $newCurDate1 = '2000-02-01T00:00:00.000+00:00'; + $newUpdatedAt1 = '2000-02-02T02:02:02.000+00:00'; + $updated1 = $database->withPreserveDates(function () use ($database, $col, $newCurDate1, $newUpdatedAt1) { + return $database->updateDocument($col, 'd1', new Document([ + 'curDate' => $newCurDate1, + '$updatedAt' => $newUpdatedAt1, + ])); + }); + $this->assertEquals($newCurDate1, $updated1->getAttribute('curDate')); + $this->assertEquals($newUpdatedAt1, $updated1->getAttribute('$updatedAt')); + $refetched1 = $database->getDocument($col, 'd1'); + $this->assertEquals($newCurDate1, $refetched1->getAttribute('curDate')); + $this->assertEquals($newUpdatedAt1, $refetched1->getAttribute('$updatedAt')); + + // updateDocuments with preserved $updatedAt over a subset + $bulkCurDate = '2001-01-01T00:00:00.000+00:00'; + $bulkUpdatedAt = '2001-01-02T00:00:00.000+00:00'; + $updatedCount = $database->withPreserveDates(function () use ($database, $col, $bulkCurDate, $bulkUpdatedAt) { + return $database->updateDocuments( + $col, + new Document([ + 'curDate' => $bulkCurDate, + '$updatedAt' => $bulkUpdatedAt, + ]), + [Query::equal('$id', ['d2', 'd3'])] + ); + }); + $this->assertEquals(2, $updatedCount); + $afterBulk2 = $database->getDocument($col, 'd2'); + $afterBulk3 = $database->getDocument($col, 'd3'); + $this->assertEquals($bulkCurDate, $afterBulk2->getAttribute('curDate')); + $this->assertEquals($bulkUpdatedAt, $afterBulk2->getAttribute('$updatedAt')); + $this->assertEquals($bulkCurDate, $afterBulk3->getAttribute('curDate')); + $this->assertEquals($bulkUpdatedAt, $afterBulk3->getAttribute('$updatedAt')); + + // upsertDocument: create new then update existing with preserved dates + $createdAt4 = '2003-03-03T03:03:03.000+00:00'; + $updatedAt4 = '2003-03-04T04:04:04.000+00:00'; + $curDate4 = '2003-03-05T05:05:05.000+00:00'; + $up1 = $database->withPreserveDates(function () use ($database, $col, $permissions, $createdAt4, $updatedAt4, $curDate4) { + return $database->upsertDocument($col, new Document([ + '$id' => 'd4', + '$permissions' => $permissions, + '$createdAt' => $createdAt4, + '$updatedAt' => $updatedAt4, + 'curDate' => $curDate4, + ])); + }); + $this->assertEquals('d4', $up1->getId()); + $this->assertEquals($curDate4, $up1->getAttribute('curDate')); + $this->assertEquals($createdAt4, $up1->getAttribute('$createdAt')); + $this->assertEquals($updatedAt4, $up1->getAttribute('$updatedAt')); + + $updatedAt4b = '2003-03-06T06:06:06.000+00:00'; + $curDate4b = '2003-03-07T07:07:07.000+00:00'; + $up2 = $database->withPreserveDates(function () use ($database, $col, $updatedAt4b, $curDate4b) { + return $database->upsertDocument($col, new Document([ + '$id' => 'd4', + 'curDate' => $curDate4b, + '$updatedAt' => $updatedAt4b, + ])); + }); + $this->assertEquals($curDate4b, $up2->getAttribute('curDate')); + $this->assertEquals($updatedAt4b, $up2->getAttribute('$updatedAt')); + $refetched4 = $database->getDocument($col, 'd4'); + $this->assertEquals($curDate4b, $refetched4->getAttribute('curDate')); + $this->assertEquals($updatedAt4b, $refetched4->getAttribute('$updatedAt')); + + // upsertDocuments: mix create and update with preserved dates + $createdAt5 = '2004-04-01T01:01:01.000+00:00'; + $updatedAt5 = '2004-04-02T02:02:02.000+00:00'; + $curDate5 = '2004-04-03T03:03:03.000+00:00'; + $updatedAt2b = '2001-02-08T08:08:08.000+00:00'; + $curDate2b = '2001-02-09T09:09:09.000+00:00'; + + $upCount = $database->withPreserveDates(function () use ($database, $col, $permissions, $createdAt5, $updatedAt5, $curDate5, $updatedAt2b, $curDate2b) { + return $database->upsertDocuments($col, [ + new Document([ + '$id' => 'd5', + '$permissions' => $permissions, + '$createdAt' => $createdAt5, + '$updatedAt' => $updatedAt5, + 'curDate' => $curDate5, + ]), + new Document([ + '$id' => 'd2', + '$updatedAt' => $updatedAt2b, + 'curDate' => $curDate2b, + ]), + ]); + }); + $this->assertEquals(2, $upCount); + + $fetched5 = $database->getDocument($col, 'd5'); + $this->assertEquals($curDate5, $fetched5->getAttribute('curDate')); + $this->assertEquals($createdAt5, $fetched5->getAttribute('$createdAt')); + $this->assertEquals($updatedAt5, $fetched5->getAttribute('$updatedAt')); + + $fetched2b = $database->getDocument($col, 'd2'); + $this->assertEquals($curDate2b, $fetched2b->getAttribute('curDate')); + $this->assertEquals($updatedAt2b, $fetched2b->getAttribute('$updatedAt')); + + // increase/decrease should not affect date types; ensure they remain strings + $afterInc = $database->increaseDocumentAttribute($col, 'd1', 'counter', 5); + $this->assertEquals(5, $afterInc->getAttribute('counter')); + $this->assertTrue(is_string($afterInc->getAttribute('curDate'))); + $this->assertTrue(is_string($afterInc->getAttribute('$createdAt'))); + $this->assertTrue(is_string($afterInc->getAttribute('$updatedAt'))); + + $afterIncFetched = $database->getDocument($col, 'd1'); + $this->assertEquals(5, $afterIncFetched->getAttribute('counter')); + $this->assertTrue(is_string($afterIncFetched->getAttribute('curDate'))); + $this->assertTrue(is_string($afterIncFetched->getAttribute('$createdAt'))); + $this->assertTrue(is_string($afterIncFetched->getAttribute('$updatedAt'))); + + $afterDec = $database->decreaseDocumentAttribute($col, 'd1', 'counter', 2); + $this->assertEquals(3, $afterDec->getAttribute('counter')); + $this->assertTrue(is_string($afterDec->getAttribute('curDate'))); + $this->assertTrue(is_string($afterDec->getAttribute('$createdAt'))); + $this->assertTrue(is_string($afterDec->getAttribute('$updatedAt'))); + + $afterDecFetched = $database->getDocument($col, 'd1'); + $this->assertEquals(3, $afterDecFetched->getAttribute('counter')); + $this->assertTrue(is_string($afterDecFetched->getAttribute('curDate'))); + $this->assertTrue(is_string($afterDecFetched->getAttribute('$createdAt'))); + $this->assertTrue(is_string($afterDecFetched->getAttribute('$updatedAt'))); + + $database->deleteCollection($col); + } + } From 0dceeee2f015db38225b15e54af4b6899a44c198 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 30 Sep 2025 13:10:40 +0530 Subject: [PATCH 08/15] reverted changes --- src/Database/Database.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 4c776d97c..01e19047c 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4049,17 +4049,17 @@ public function createDocuments( $batch = $this->adapter->getSequences($collection->getId(), $batch); - foreach ($batch as $doc) { - $doc = $this->adapter->castingAfter($collection, $doc); + foreach ($batch as $document) { + $document = $this->adapter->castingAfter($collection, $document); if ($this->resolveRelationships) { - $doc = $this->silent(fn () => $this->populateDocumentRelationships($collection, $doc)); + $document = $this->silent(fn () => $this->populateDocumentRelationships($collection, $document)); } - $doc = $this->casting($collection, $doc); - $doc = $this->decode($collection, $doc); + $document = $this->casting($collection, $document); + $document = $this->decode($collection, $document); try { - $onNext && $onNext($doc); + $onNext && $onNext($document); } catch (\Throwable $e) { $onError ? $onError($e) : throw $e; } From f0729d4604fa693d2b6f846d3e3bfe62550804af Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 30 Sep 2025 18:22:43 +0530 Subject: [PATCH 09/15] * updated filters to prioritize the max query values validation first * mongo contains query fix --- src/Database/Adapter/Mongo.php | 13 ++++++++++++- src/Database/Validator/Query/Filter.php | 10 +++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 60e186ca2..96295f2dd 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -2201,7 +2201,18 @@ protected function buildFilter(Query $query): array $filter[$attribute]['$nin'] = $value; } elseif ($operator == '$in') { if ($query->getMethod() === Query::TYPE_CONTAINS && !$query->onArray()) { - $filter[$attribute]['$regex'] = new Regex(".*{$this->escapeWildcards($value)}.*", 'i'); + // contains support array values + if (is_array($value)) { + $filter['$or'] = array_map(function ($val) use ($attribute) { + return [ + $attribute => [ + '$regex' => new Regex(".*{$this->escapeWildcards($val)}.*", 'i') + ] + ]; + }, $value); + } else { + $filter[$attribute]['$regex'] = new Regex(".*{$this->escapeWildcards($value)}.*", 'i'); + } } else { $filter[$attribute]['$in'] = $query->getValues(); } diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index b040f70c6..dd64001e4 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -95,16 +95,16 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s $attribute = \explode('.', $attribute)[0]; } - if (!$this->supportForAttributes && !isset($this->schema[$attribute])) { - return true; - } - $attributeSchema = $this->schema[$attribute]; - if (count($values) > $this->maxValuesCount) { $this->message = 'Query on attribute has greater than ' . $this->maxValuesCount . ' values: ' . $attribute; return false; } + if (!$this->supportForAttributes && !isset($this->schema[$attribute])) { + return true; + } + $attributeSchema = $this->schema[$attribute]; + $attributeType = $attributeSchema['type']; // If the query method is spatial-only, the attribute must be a spatial type From 4103174f0d99f1d1761312e320e92bda22647ba2 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 30 Sep 2025 18:40:04 +0530 Subject: [PATCH 10/15] linting --- tests/e2e/Adapter/Scopes/SchemalessTests.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/Adapter/Scopes/SchemalessTests.php b/tests/e2e/Adapter/Scopes/SchemalessTests.php index 111966783..9c67a3e76 100644 --- a/tests/e2e/Adapter/Scopes/SchemalessTests.php +++ b/tests/e2e/Adapter/Scopes/SchemalessTests.php @@ -1187,5 +1187,4 @@ public function testSchemalessDates(): void $database->deleteCollection($col); } - } From b9e408aa15abcb71ef337e314b36a25e73eae5f0 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 6 Oct 2025 12:52:16 +0530 Subject: [PATCH 11/15] updated lock --- composer.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/composer.lock b/composer.lock index 81cf8096c..f125acff0 100644 --- a/composer.lock +++ b/composer.lock @@ -410,16 +410,16 @@ }, { "name": "open-telemetry/api", - "version": "1.6.0", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "ee17d937652eca06c2341b6fadc0f74c1c1a5af2" + "reference": "610b79ad9d6d97e8368bcb6c4d42394fbb87b522" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/ee17d937652eca06c2341b6fadc0f74c1c1a5af2", - "reference": "ee17d937652eca06c2341b6fadc0f74c1c1a5af2", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/610b79ad9d6d97e8368bcb6c4d42394fbb87b522", + "reference": "610b79ad9d6d97e8368bcb6c4d42394fbb87b522", "shasum": "" }, "require": { @@ -439,7 +439,7 @@ ] }, "branch-alias": { - "dev-main": "1.4.x-dev" + "dev-main": "1.7.x-dev" } }, "autoload": { @@ -476,7 +476,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-09-19T00:05:49+00:00" + "time": "2025-10-02T23:44:28+00:00" }, { "name": "open-telemetry/context", @@ -666,22 +666,22 @@ }, { "name": "open-telemetry/sdk", - "version": "1.8.0", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "105c6e81e3d86150bd5704b00c7e4e165e957b89" + "reference": "8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/105c6e81e3d86150bd5704b00c7e4e165e957b89", - "reference": "105c6e81e3d86150bd5704b00c7e4e165e957b89", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e", + "reference": "8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e", "shasum": "" }, "require": { "ext-json": "*", "nyholm/psr7-server": "^1.1", - "open-telemetry/api": "^1.6", + "open-telemetry/api": "^1.7", "open-telemetry/context": "^1.4", "open-telemetry/sem-conv": "^1.0", "php": "^8.1", @@ -716,7 +716,7 @@ ] }, "branch-alias": { - "dev-main": "1.0.x-dev" + "dev-main": "1.9.x-dev" } }, "autoload": { @@ -759,7 +759,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-09-19T00:05:49+00:00" + "time": "2025-10-02T23:44:28+00:00" }, { "name": "open-telemetry/sem-conv", From 64135d91589352e52d8c23eae359297997d51e2c Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 6 Oct 2025 13:04:06 +0530 Subject: [PATCH 12/15] pr followups * updated index tests skipping condition for the duplicate indexes * removed redundant double quote for linting * removed redundant enforced schema creation as it is already present --- tests/e2e/Adapter/Scopes/IndexTests.php | 5 +-- tests/e2e/Adapter/Scopes/SchemalessTests.php | 43 +++----------------- 2 files changed, 7 insertions(+), 41 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/IndexTests.php b/tests/e2e/Adapter/Scopes/IndexTests.php index 12d9450e4..b12420faf 100644 --- a/tests/e2e/Adapter/Scopes/IndexTests.php +++ b/tests/e2e/Adapter/Scopes/IndexTests.php @@ -175,13 +175,10 @@ public function testIndexValidation(): void $database->getAdapter()->getSupportForMultipleFulltextIndexes(), $database->getAdapter()->getSupportForIdenticalIndexes() ); - if ($database->getAdapter()->getSupportForAttributes()) { + if ($database->getAdapter()->getSupportForIdenticalIndexes()) { $errorMessage = 'Index length 701 is larger than the size for title1: 700"'; $this->assertFalse($validator->isValid($indexes[0])); $this->assertEquals($errorMessage, $validator->getDescription()); - } - - if ($database->getAdapter()->getSupportForAttributes()) { try { $database->createCollection($collection->getId(), $attributes, $indexes, [ Permission::read(Role::any()), diff --git a/tests/e2e/Adapter/Scopes/SchemalessTests.php b/tests/e2e/Adapter/Scopes/SchemalessTests.php index 9c67a3e76..1902f4e5d 100644 --- a/tests/e2e/Adapter/Scopes/SchemalessTests.php +++ b/tests/e2e/Adapter/Scopes/SchemalessTests.php @@ -27,7 +27,7 @@ public function testSchemalessDocumentOperation(): void return; } - $colName = uniqid("schemaless"); + $colName = uniqid('schemaless'); $database->createCollection($colName); $database->createAttribute($colName, 'key', Database::VAR_STRING, 50, true); $database->createAttribute($colName, 'value', Database::VAR_STRING, 50, false, 'value'); @@ -97,7 +97,6 @@ public function testSchemalessDocumentOperation(): void $this->assertEquals(2, $doc1AfterUpsert->getAttribute('extraU')); // Increase/Decrease numeric attribute: add numeric attribute and mutate it - $database->createAttribute($colName, 'counter', Database::VAR_INTEGER, 0, false, 0); $docS = $database->getDocument($colName, 'docS'); $this->assertEquals(0, $docS->getAttribute('counter')); $docS = $database->increaseDocumentAttribute($colName, 'docS', 'counter', 5); @@ -127,7 +126,7 @@ public function testSchemalessDocumentInvalidInteralAttributeValidation(): void return; } - $colName = uniqid("schemaless"); + $colName = uniqid('schemaless'); $database->createCollection($colName); try { $docs = [ @@ -155,36 +154,6 @@ public function testSchemalessDocumentInvalidInteralAttributeValidation(): void } - public function testSchemaEnforcedDocumentCreation(): void - { - /** @var Database $database */ - $database = static::getDatabase(); - - if (!$database->getAdapter()->getSupportForAttributes()) { - $this->expectNotToPerformAssertions(); - return; - } - - $colName = uniqid("schema"); - $database->createCollection($colName); - $database->createAttribute($colName, 'key', Database::VAR_STRING, 50, true); - $database->createAttribute($colName, 'value', Database::VAR_STRING, 50, false, 'value'); - - $permissions = [Permission::read(Role::any()), Permission::write(Role::any()), Permission::update(Role::any())]; - - // Extra attributes should fail - $docs = [ - new Document(['$id' => 'doc11', 'key' => 'doc1', 'title' => 'doc1', '$permissions' => $permissions]), - new Document(['$id' => 'doc21', 'key' => 'doc2', 'moviename' => 'doc2', 'moviedescription' => 'test', '$permissions' => $permissions]), - new Document(['$id' => 'doc31', 'key' => 'doc3', '$permissions' => $permissions]), - ]; - - $this->expectException(StructureException::class); - $database->createDocuments($colName, $docs); - - $database->deleteCollection($colName); - } - public function testSchemalessSelectionOnUnknownAttributes(): void { /** @var Database $database */ @@ -195,7 +164,7 @@ public function testSchemalessSelectionOnUnknownAttributes(): void return; } - $colName = uniqid("schemaless"); + $colName = uniqid('schemaless'); $database->createCollection($colName); $permissions = [Permission::read(Role::any()), Permission::write(Role::any()), Permission::update(Role::any())]; $docs = [ @@ -215,8 +184,8 @@ public function testSchemalessSelectionOnUnknownAttributes(): void foreach ($docs as $doc) { $this->assertNull($doc->getAttribute('freeC')); // since not selected - $this->assertNull($doc->getAttribute('freeA')); - $this->assertNull($doc->getAttribute('freeB')); + $this->assertArrayNotHasKey('freeA', $doc->getAttributes()); + $this->assertArrayNotHasKey('freeB', $doc->getAttributes()); } $docA = $database->find($colName, [ @@ -229,7 +198,7 @@ public function testSchemalessSelectionOnUnknownAttributes(): void Query::equal('$id', ['doc1']), Query::select(['freeC']) ]); - $this->assertNull($docC[0]->getAttribute('freeC')); + $this->assertArrayNotHasKey('freeC', $docC[0]->getAttributes()); } public function testSchemalessIncrement(): void From 8dbfb0dbac54f0bc325afad812d8cec20f6a42b7 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 6 Oct 2025 14:09:05 +0530 Subject: [PATCH 13/15] * updated regex in contains if array to createSafeRegex * updated index test --- src/Database/Adapter/Mongo.php | 2 +- tests/e2e/Adapter/Scopes/IndexTests.php | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index e9c7334a9..4a80ff751 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -2333,7 +2333,7 @@ protected function buildFilter(Query $query): array $filter['$or'] = array_map(function ($val) use ($attribute) { return [ $attribute => [ - '$regex' => new Regex(".*{$this->escapeWildcards($val)}.*", 'i') + '$regex' => $this->createSafeRegex($val, '.*%s.*', 'i') ] ]; }, $value); diff --git a/tests/e2e/Adapter/Scopes/IndexTests.php b/tests/e2e/Adapter/Scopes/IndexTests.php index b12420faf..e3fefbd6b 100644 --- a/tests/e2e/Adapter/Scopes/IndexTests.php +++ b/tests/e2e/Adapter/Scopes/IndexTests.php @@ -244,6 +244,15 @@ public function testIndexValidation(): void 'indexes' => $indexes ]); + // not using $indexes[0] as the index validator skips indexes with same id + $newIndex = new Document([ + '$id' => ID::custom('newIndex1'), + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['title1', 'integer'], + 'lengths' => [], + 'orders' => [], + ]); + $validator = new Index( $attributes, $indexes, @@ -258,7 +267,7 @@ public function testIndexValidation(): void $database->getAdapter()->getSupportForIdenticalIndexes() ); - $this->assertFalse($validator->isValid($indexes[0])); + $this->assertFalse($validator->isValid($newIndex)); if ($database->getAdapter()->getSupportForAttributes()) { $this->assertEquals('Attribute "integer" cannot be part of a fulltext index, must be of type string', $validator->getDescription()); From e04a6ff0a53f430bf36b046b945d3ba92320a564 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 6 Oct 2025 14:33:02 +0530 Subject: [PATCH 14/15] linting --- tests/e2e/Adapter/Scopes/SchemalessTests.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/SchemalessTests.php b/tests/e2e/Adapter/Scopes/SchemalessTests.php index 1902f4e5d..ee0985682 100644 --- a/tests/e2e/Adapter/Scopes/SchemalessTests.php +++ b/tests/e2e/Adapter/Scopes/SchemalessTests.php @@ -66,8 +66,7 @@ public function testSchemalessDocumentOperation(): void $this->assertEquals('doc2', $doc21->getAttribute('moviename')); $this->assertEquals('test', $doc21->getAttribute('moviedescription')); - $updated = $database->updateDocument($colName, 'doc31', new Document(['moviename' => 'updated'])) - ; + $updated = $database->updateDocument($colName, 'doc31', new Document(['moviename' => 'updated'])); $this->assertEquals('updated', $updated->getAttribute('moviename')); $this->assertTrue($database->deleteDocument($colName, 'doc21')); From 1ce871b367fd70dfc8eff747d231e1224f78bfba Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 6 Oct 2025 16:44:15 +0530 Subject: [PATCH 15/15] linting --- tests/unit/Validator/IndexTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/Validator/IndexTest.php b/tests/unit/Validator/IndexTest.php index 62f42ba07..987297ae2 100644 --- a/tests/unit/Validator/IndexTest.php +++ b/tests/unit/Validator/IndexTest.php @@ -382,11 +382,11 @@ public function testIndexWithNoAttributeSupport(): void ], ]); - $validator = new Index(attributes:$collection->getAttribute('attributes'), indexes:$collection->getAttribute('indexes'), maxLength:768); + $validator = new Index(attributes: $collection->getAttribute('attributes'), indexes: $collection->getAttribute('indexes'), maxLength: 768); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); - $validator = new Index(attributes: $collection->getAttribute('attributes'), indexes:$collection->getAttribute('indexes'), maxLength: 768, supportForAttributes:false); + $validator = new Index(attributes: $collection->getAttribute('attributes'), indexes: $collection->getAttribute('indexes'), maxLength: 768, supportForAttributes: false); $index = $collection->getAttribute('indexes')[0]; $this->assertTrue($validator->isValid($index)); }