diff --git a/src/Database/Database.php b/src/Database/Database.php index 0bee028ae..4a7154715 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1411,6 +1411,7 @@ public function createCollection(string $id, array $attributes = [], array $inde $this->adapter->getSupportForSpatialAttributes(), $this->adapter->getSupportForSpatialIndexNull(), $this->adapter->getSupportForSpatialIndexOrder(), + $this->adapter->getSupportForAttributes() ); foreach ($indexes as $index) { if (!$validator->isValid($index)) { @@ -2420,6 +2421,7 @@ public function updateAttribute(string $collection, string $id, ?string $type = $this->adapter->getSupportForSpatialAttributes(), $this->adapter->getSupportForSpatialIndexNull(), $this->adapter->getSupportForSpatialIndexOrder(), + $this->adapter->getSupportForAttributes() ); foreach ($indexes as $index) { @@ -3363,6 +3365,7 @@ public function createIndex(string $collection, string $id, string $type, array $this->adapter->getSupportForSpatialAttributes(), $this->adapter->getSupportForSpatialIndexNull(), $this->adapter->getSupportForSpatialIndexOrder(), + $this->adapter->getSupportForAttributes() ); if (!$validator->isValid($index)) { throw new IndexException($validator->getDescription()); @@ -3906,6 +3909,7 @@ public function createDocument(string $collection, Document $document): Document $this->adapter->getIdAttributeType(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes() ); if (!$structure->isValid($document)) { throw new StructureException($structure->getDescription()); @@ -4006,6 +4010,7 @@ public function createDocuments( $this->adapter->getIdAttributeType(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes() ); if (!$validator->isValid($document)) { throw new StructureException($validator->getDescription()); @@ -4559,6 +4564,7 @@ public function updateDocument(string $collection, string $id, Document $documen $this->adapter->getIdAttributeType(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes() ); if (!$structureValidator->isValid($document)) { // Make sure updated structure still apply collection rules (if any) throw new StructureException($structureValidator->getDescription()); @@ -4693,6 +4699,7 @@ public function updateDocuments( $this->adapter->getIdAttributeType(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes() ); if (!$validator->isValid($updates)) { @@ -5404,6 +5411,7 @@ public function upsertDocumentsWithIncrease( $this->adapter->getIdAttributeType(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes() ); if (!$validator->isValid($document)) { @@ -6397,6 +6405,7 @@ public function find(string $collection, array $queries = [], string $forPermiss $this->maxQueryValues, $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes() ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index bab80c173..e22bfaaff 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -30,6 +30,7 @@ class Index extends Validator protected bool $spatialIndexOrderSupport; + protected bool $supportForAttributes; /** * @param array $attributes * @param int $maxLength @@ -40,7 +41,7 @@ class Index extends Validator * @param bool $spatialIndexOrderSupport * @throws DatabaseException */ - public function __construct(array $attributes, int $maxLength, array $reservedKeys = [], bool $arrayIndexSupport = false, bool $spatialIndexSupport = false, bool $spatialIndexNullSupport = false, bool $spatialIndexOrderSupport = false) + public function __construct(array $attributes, int $maxLength, array $reservedKeys = [], bool $arrayIndexSupport = false, bool $spatialIndexSupport = false, bool $spatialIndexNullSupport = false, bool $spatialIndexOrderSupport = false, bool $supportForAttributes = true) { $this->maxLength = $maxLength; $this->reservedKeys = $reservedKeys; @@ -48,6 +49,7 @@ public function __construct(array $attributes, int $maxLength, array $reservedKe $this->spatialIndexSupport = $spatialIndexSupport; $this->spatialIndexNullSupport = $spatialIndexNullSupport; $this->spatialIndexOrderSupport = $spatialIndexOrderSupport; + $this->supportForAttributes = $supportForAttributes; foreach ($attributes as $attribute) { $key = \strtolower($attribute->getAttribute('key', $attribute->getAttribute('$id'))); @@ -75,7 +77,7 @@ public function getDescription(): string public function checkAttributesNotFound(Document $index): bool { foreach ($index->getAttribute('attributes', []) as $attribute) { - if (!isset($this->attributes[\strtolower($attribute)])) { + if ($this->supportForAttributes && !isset($this->attributes[\strtolower($attribute)])) { $this->message = 'Invalid index attribute "' . $attribute . '" not found'; return false; } @@ -123,6 +125,9 @@ public function checkDuplicatedAttributes(Document $index): bool */ public function checkFulltextIndexNonString(Document $index): bool { + if (!$this->supportForAttributes) { + return true; + } if ($index->getAttribute('type') === Database::INDEX_FULLTEXT) { foreach ($index->getAttribute('attributes', []) as $attribute) { $attribute = $this->attributes[\strtolower($attribute)] ?? new Document(); @@ -141,6 +146,9 @@ public function checkFulltextIndexNonString(Document $index): bool */ public function checkArrayIndex(Document $index): bool { + if (!$this->supportForAttributes) { + return true; + } $attributes = $index->getAttribute('attributes', []); $orders = $index->getAttribute('orders', []); $lengths = $index->getAttribute('lengths', []); diff --git a/src/Database/Validator/Queries/Documents.php b/src/Database/Validator/Queries/Documents.php index 289ccbe5b..ca3127312 100644 --- a/src/Database/Validator/Queries/Documents.php +++ b/src/Database/Validator/Queries/Documents.php @@ -30,6 +30,7 @@ public function __construct( int $maxValuesCount = 100, \DateTime $minAllowedDate = new \DateTime('0000-01-01'), \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), + bool $supportForAttributes = true ) { $attributes[] = new Document([ '$id' => '$id', @@ -66,9 +67,10 @@ public function __construct( $maxValuesCount, $minAllowedDate, $maxAllowedDate, + $supportForAttributes ), - new Order($attributes), - new Select($attributes), + new Order($attributes, $supportForAttributes), + new Select($attributes, $supportForAttributes), ]; parent::__construct($attributes, $indexes, $validators); diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index 9c60f551c..b040f70c6 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -31,6 +31,7 @@ public function __construct( private readonly int $maxValuesCount = 100, private readonly \DateTime $minAllowedDate = new \DateTime('0000-01-01'), private readonly \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), + private bool $supportForAttributes = true ) { foreach ($attributes as $attribute) { $this->schema[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); @@ -67,7 +68,7 @@ protected function isValidAttribute(string $attribute): bool } // Search for attribute in schema - if (!isset($this->schema[$attribute])) { + if ($this->supportForAttributes && !isset($this->schema[$attribute])) { $this->message = 'Attribute not found in schema: ' . $attribute; return false; } @@ -94,6 +95,9 @@ 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) { diff --git a/src/Database/Validator/Query/Order.php b/src/Database/Validator/Query/Order.php index f0e7f2d56..6a4830cf5 100644 --- a/src/Database/Validator/Query/Order.php +++ b/src/Database/Validator/Query/Order.php @@ -14,8 +14,9 @@ class Order extends Base /** * @param array $attributes + * @param bool $supportForAttributes */ - public function __construct(array $attributes = []) + public function __construct(array $attributes = [], protected bool $supportForAttributes = true) { foreach ($attributes as $attribute) { $this->schema[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); @@ -29,7 +30,7 @@ public function __construct(array $attributes = []) protected function isValidAttribute(string $attribute): bool { // Search for attribute in schema - if (!isset($this->schema[$attribute])) { + if ($this->supportForAttributes && !isset($this->schema[$attribute])) { $this->message = 'Attribute not found in schema: ' . $attribute; return false; } diff --git a/src/Database/Validator/Query/Select.php b/src/Database/Validator/Query/Select.php index 40572b828..b0ed9e564 100644 --- a/src/Database/Validator/Query/Select.php +++ b/src/Database/Validator/Query/Select.php @@ -29,8 +29,9 @@ class Select extends Base /** * @param array $attributes + * @param bool $supportForAttributes */ - public function __construct(array $attributes = []) + public function __construct(array $attributes = [], protected bool $supportForAttributes = true) { foreach ($attributes as $attribute) { $this->schema[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); @@ -89,7 +90,7 @@ public function isValid($value): bool continue; } - if (!isset($this->schema[$attribute]) && $attribute !== '*') { + if ($this->supportForAttributes && !isset($this->schema[$attribute]) && $attribute !== '*') { $this->message = 'Attribute not found in schema: ' . $attribute; return false; } diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index cfb12fa3a..421eafd83 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -106,6 +106,7 @@ public function __construct( private readonly string $idAttributeType, private readonly \DateTime $minAllowedDate = new \DateTime('0000-01-01'), private readonly \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), + private bool $supportForAttributes = true ) { } @@ -251,7 +252,11 @@ public function isValid($document): bool */ protected function checkForAllRequiredValues(array $structure, array $attributes, array &$keys): bool { - foreach ($attributes as $key => $attribute) { // Check all required attributes are set + if (!$this->supportForAttributes) { + return true; + } + + foreach ($attributes as $attribute) { // Check all required attributes are set $name = $attribute['$id'] ?? ''; $required = $attribute['required'] ?? false; @@ -276,6 +281,9 @@ protected function checkForAllRequiredValues(array $structure, array $attributes */ protected function checkForUnknownAttributes(array $structure, array $keys): bool { + if (!$this->supportForAttributes) { + return true; + } foreach ($structure as $key => $value) { if (!array_key_exists($key, $keys)) { // Check no unknown attributes are set $this->message = 'Unknown attribute: "'.$key.'"'; @@ -357,8 +365,10 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys) break; default: - $this->message = 'Unknown attribute type "'.$type.'"'; - return false; + if ($this->supportForAttributes) { + $this->message = 'Unknown attribute type "'.$type.'"'; + return false; + } } /** Error message label, either 'format' or 'type' */ diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index 25ee025d8..60eb25f77 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -392,6 +392,11 @@ public function testUpdateAttributeRequired(): void /** @var Database $database */ $database = static::getDatabase(); + if (!$database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + $database->updateAttributeRequired('flowers', 'inStock', true); $this->expectExceptionMessage('Invalid document structure: Missing required attribute "inStock"'); @@ -448,7 +453,10 @@ public function testUpdateAttributeFormat(): void { /** @var Database $database */ $database = static::getDatabase(); - + if (!$database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } $database->createAttribute('flowers', 'price', Database::VAR_INTEGER, 0, false); $doc = $database->createDocument('flowers', new Document([ @@ -648,6 +656,10 @@ public function testUpdateAttributeRename(): void { /** @var Database $database */ $database = static::getDatabase(); + if (!$database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } $database->createCollection('rename_test'); @@ -1330,7 +1342,9 @@ public function testArrayAttribute(): void $database->createDocument($collection, new Document([])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - $this->assertEquals('Invalid document structure: Missing required attribute "booleans"', $e->getMessage()); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->assertEquals('Invalid document structure: Missing required attribute "booleans"', $e->getMessage()); + } } $database->updateAttribute($collection, 'booleans', required: false); @@ -1350,7 +1364,9 @@ public function testArrayAttribute(): void ])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - $this->assertEquals('Invalid document structure: Attribute "short[\'0\']" has invalid type. Value must be a valid string and no longer than 5 chars', $e->getMessage()); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->assertEquals('Invalid document structure: Attribute "short[\'0\']" has invalid type. Value must be a valid string and no longer than 5 chars', $e->getMessage()); + } } try { @@ -1359,7 +1375,9 @@ public function testArrayAttribute(): void ])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - $this->assertEquals('Invalid document structure: Attribute "names[\'1\']" has invalid type. Value must be a valid string and no longer than 255 chars', $e->getMessage()); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->assertEquals('Invalid document structure: Attribute "names[\'1\']" has invalid type. Value must be a valid string and no longer than 255 chars', $e->getMessage()); + } } try { @@ -1368,7 +1386,9 @@ public function testArrayAttribute(): void ])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid integer', $e->getMessage()); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid integer', $e->getMessage()); + } } try { @@ -1377,7 +1397,9 @@ public function testArrayAttribute(): void ])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid range between 0 and 2,147,483,647', $e->getMessage()); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid range between 0 and 2,147,483,647', $e->getMessage()); + } } $database->createDocument($collection, new Document([ @@ -1451,7 +1473,9 @@ public function testArrayAttribute(): void if ($database->getAdapter()->getSupportForIndexArray()) { try { $database->createIndex($collection, 'indx', Database::INDEX_FULLTEXT, ['names']); - $this->fail('Failed to throw exception'); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->fail('Failed to throw exception'); + } } catch (Throwable $e) { if ($database->getAdapter()->getSupportForFulltextIndex()) { $this->assertEquals('"Fulltext" index is forbidden on array attributes', $e->getMessage()); @@ -1462,9 +1486,15 @@ public function testArrayAttribute(): void try { $database->createIndex($collection, 'indx', Database::INDEX_KEY, ['numbers', 'names'], [100,100]); - $this->fail('Failed to throw exception'); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->fail('Failed to throw exception'); + } } catch (Throwable $e) { - $this->assertEquals('An index may only contain one array attribute', $e->getMessage()); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->assertEquals('An index may only contain one array attribute', $e->getMessage()); + } else { + $this->assertEquals('Index already exists', $e->getMessage()); + } } } @@ -1498,8 +1528,10 @@ public function testArrayAttribute(): void $database->createIndex($collection, 'indx3', Database::INDEX_KEY, ['names'], [255], ['desc']); try { - $database->createIndex($collection, 'indx4', Database::INDEX_KEY, ['age', 'names'], [10, 255], []); - $this->fail('Failed to throw exception'); + if ($database->getAdapter()->getSupportForAttributes()) { + $database->createIndex($collection, 'indx4', Database::INDEX_KEY, ['age', 'names'], [10, 255], []); + $this->fail('Failed to throw exception'); + } } catch (Throwable $e) { $this->assertEquals('Cannot set a length on "integer" attributes', $e->getMessage()); } @@ -1566,17 +1598,22 @@ public function testCreateDatetime(): void $database = static::getDatabase(); $database->createCollection('datetime'); - - $this->assertEquals(true, $database->createAttribute('datetime', 'date', Database::VAR_DATETIME, 0, true, null, true, false, null, [], ['datetime'])); - $this->assertEquals(true, $database->createAttribute('datetime', 'date2', Database::VAR_DATETIME, 0, false, null, true, false, null, [], ['datetime'])); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->assertEquals(true, $database->createAttribute('datetime', 'date', Database::VAR_DATETIME, 0, true, null, true, false, null, [], ['datetime'])); + $this->assertEquals(true, $database->createAttribute('datetime', 'date2', Database::VAR_DATETIME, 0, false, null, true, false, null, [], ['datetime'])); + } try { $database->createDocument('datetime', new Document([ 'date' => ['2020-01-01'], // array ])); - $this->fail('Failed to throw exception'); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->fail('Failed to throw exception'); + } } catch (Exception $e) { - $this->assertInstanceOf(StructureException::class, $e); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->assertInstanceOf(StructureException::class, $e); + } } $doc = $database->createDocument('datetime', new Document([ @@ -1619,20 +1656,29 @@ public function testCreateDatetime(): void try { $database->createDocument('datetime', new Document([ - 'date' => "1975-12-06 00:00:61" // 61 seconds is invalid + '$id' => 'datenew1', + 'date' => "1975-12-06 00:00:61", // 61 seconds is invalid, ])); - $this->fail('Failed to throw exception'); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->fail('Failed to throw exception'); + } } catch (Exception $e) { - $this->assertInstanceOf(StructureException::class, $e); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->assertInstanceOf(StructureException::class, $e); + } } try { $database->createDocument('datetime', new Document([ 'date' => '+055769-02-14T17:56:18.000Z' ])); - $this->fail('Failed to throw exception'); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->fail('Failed to throw exception'); + } } catch (Exception $e) { - $this->assertInstanceOf(StructureException::class, $e); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->assertInstanceOf(StructureException::class, $e); + } } $invalidDates = [ @@ -1656,7 +1702,9 @@ public function testCreateDatetime(): void $database->find('datetime', [ Query::equal('date', [$date]) ]); - $this->fail('Failed to throw exception'); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->fail('Failed to throw exception'); + } } catch (Throwable $e) { $this->assertTrue($e instanceof QueryException); $this->assertEquals('Invalid query: Query value is invalid for attribute "date"', $e->getMessage()); diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 03461b12f..db308431f 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -229,8 +229,10 @@ public function testCreateDocument(): Document ])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - $this->assertTrue($e instanceof StructureException); - $this->assertStringContainsString('Invalid document structure: Attribute "float_unsigned" has invalid type. Value must be a valid range between 0 and', $e->getMessage()); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->assertTrue($e instanceof StructureException); + $this->assertStringContainsString('Invalid document structure: Attribute "float_unsigned" has invalid type. Value must be a valid range between 0 and', $e->getMessage()); + } } try { @@ -248,8 +250,10 @@ public function testCreateDocument(): Document ])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - $this->assertTrue($e instanceof StructureException); - $this->assertEquals('Invalid document structure: Attribute "bigint_unsigned" has invalid type. Value must be a valid range between 0 and 9,223,372,036,854,775,807', $e->getMessage()); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->assertTrue($e instanceof StructureException); + $this->assertEquals('Invalid document structure: Attribute "bigint_unsigned" has invalid type. Value must be a valid range between 0 and 9,223,372,036,854,775,807', $e->getMessage()); + } } try { @@ -270,8 +274,10 @@ public function testCreateDocument(): Document ])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - $this->assertTrue($e instanceof StructureException); - $this->assertEquals('Invalid document structure: Attribute "$sequence" has invalid type. Invalid sequence value', $e->getMessage()); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->assertTrue($e instanceof StructureException); + $this->assertEquals('Invalid document structure: Attribute "$sequence" has invalid type. Invalid sequence value', $e->getMessage()); + } } /** @@ -933,7 +939,9 @@ public function testUpsertDocumentsAttributeMismatch(): void ]); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - $this->assertTrue($e instanceof StructureException, $e->getMessage()); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->assertTrue($e instanceof StructureException, $e->getMessage()); + } } // Ensure missing optionals on existing document is allowed @@ -5151,16 +5159,19 @@ public function testFulltextIndexWithInteger(): void { /** @var Database $database */ $database = static::getDatabase(); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->expectException(Exception::class); + if (!$this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { + $this->expectExceptionMessage('Fulltext index is not supported'); + } else { + $this->expectExceptionMessage('Attribute "integer_signed" cannot be part of a FULLTEXT index, must be of type string'); + } - $this->expectException(Exception::class); - - if (!$this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { - $this->expectExceptionMessage('Fulltext index is not supported'); + $database->createIndex('documents', 'fulltext_integer', Database::INDEX_FULLTEXT, ['string','integer_signed']); } else { - $this->expectExceptionMessage('Attribute "integer_signed" cannot be part of a FULLTEXT index, must be of type string'); + $this->expectNotToPerformAssertions(); + return; } - - $database->createIndex('documents', 'fulltext_integer', Database::INDEX_FULLTEXT, ['string','integer_signed']); } public function testEnableDisableValidation(): void @@ -6053,4 +6064,172 @@ public function testCreateUpdateDocumentsMismatch(): void } $database->deleteCollection($colName); } + + 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); + } } diff --git a/tests/e2e/Adapter/Scopes/GeneralTests.php b/tests/e2e/Adapter/Scopes/GeneralTests.php index 1664273c1..97087f3d6 100644 --- a/tests/e2e/Adapter/Scopes/GeneralTests.php +++ b/tests/e2e/Adapter/Scopes/GeneralTests.php @@ -95,6 +95,10 @@ public function testPreserveDatesUpdate(): void /** @var Database $database */ $database = static::getDatabase(); + if (!$database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } $database->setPreserveDates(true); @@ -190,6 +194,10 @@ public function testPreserveDatesCreate(): void /** @var Database $database */ $database = static::getDatabase(); + if (!$database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } $database->setPreserveDates(true); diff --git a/tests/e2e/Adapter/Scopes/IndexTests.php b/tests/e2e/Adapter/Scopes/IndexTests.php index df3207f35..3d2aa5917 100644 --- a/tests/e2e/Adapter/Scopes/IndexTests.php +++ b/tests/e2e/Adapter/Scopes/IndexTests.php @@ -249,7 +249,9 @@ public function testIndexValidation(): void try { $database->createCollection($collection->getId(), $attributes, $indexes); - $this->fail('Failed to throw exception'); + if ($database->getAdapter()->getSupportForAttributes()) { + $this->fail('Failed to throw exception'); + } } catch (Exception $e) { $this->assertEquals($errorMessage, $e->getMessage()); }