diff --git a/composer.lock b/composer.lock index 05ea6441e..894e7c790 100644 --- a/composer.lock +++ b/composer.lock @@ -336,16 +336,16 @@ }, { "name": "utopia-php/framework", - "version": "0.28.2", + "version": "0.28.4", "source": { "type": "git", "url": "https://github.com/utopia-php/framework.git", - "reference": "bc0144ff3983afa6724c43f2ce578fdbceec21f9" + "reference": "98c5469efe195aeecc63745dbf8e2f357f8cedac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/bc0144ff3983afa6724c43f2ce578fdbceec21f9", - "reference": "bc0144ff3983afa6724c43f2ce578fdbceec21f9", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/98c5469efe195aeecc63745dbf8e2f357f8cedac", + "reference": "98c5469efe195aeecc63745dbf8e2f357f8cedac", "shasum": "" }, "require": { @@ -375,9 +375,9 @@ ], "support": { "issues": "https://github.com/utopia-php/framework/issues", - "source": "https://github.com/utopia-php/framework/tree/0.28.2" + "source": "https://github.com/utopia-php/framework/tree/0.28.4" }, - "time": "2023-05-30T06:47:57+00:00" + "time": "2023-06-03T14:09:22+00:00" }, { "name": "utopia-php/mongo", @@ -907,16 +907,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.15", + "version": "1.10.18", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd" + "reference": "52b6416c579663eebdd2f1d97df21971daf3b43f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/762c4dac4da6f8756eebb80e528c3a47855da9bd", - "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/52b6416c579663eebdd2f1d97df21971daf3b43f", + "reference": "52b6416c579663eebdd2f1d97df21971daf3b43f", "shasum": "" }, "require": { @@ -965,7 +965,7 @@ "type": "tidelift" } ], - "time": "2023-05-09T15:28:01+00:00" + "time": "2023-06-07T22:00:43+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2495,16 +2495,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.2.1", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e" + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", - "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "shasum": "" }, "require": { @@ -2513,7 +2513,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -2542,7 +2542,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" }, "funding": [ { @@ -2558,7 +2558,7 @@ "type": "tidelift" } ], - "time": "2023-03-01T10:25:55+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "theseer/tokenizer", diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index d9e574851..cead857ca 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -636,4 +636,9 @@ public function escapeWildcards(string $value): string * @throws Exception */ abstract public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value, int|float|null $min = null, int|float|null $max = null): bool; + + /** + * @return int + */ + abstract public function getMaxIndexLength(): int; } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index df9481005..79457feb7 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1278,7 +1278,7 @@ protected function getSQLType(string $type, int $size, bool $signed = true): str return 'MEDIUMTEXT'; } - if ($size > 16383) { + if ($size > $this->getMaxVarcharLength()) { return 'TEXT'; } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index d44102edf..a68440e16 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1650,4 +1650,12 @@ protected function processException(Exception $e): void throw $e; } + + /** + * @return int + */ + public function getMaxIndexLength(): int + { + return 0; + } } diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 9394556c6..6bc4e9b0f 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -2,7 +2,6 @@ namespace Utopia\Database\Adapter; -use Exception; use PDOException; use Utopia\Database\Database; use Utopia\Database\Exception as DatabaseException; @@ -19,9 +18,7 @@ class MySQL extends MariaDB * @param array $attributes * * @return string - * @throws Exception - * @throws Exception - * @throws Exception + * @throws DatabaseException */ protected function getSQLIndex(string $collection, string $id, string $type, array $attributes): string { diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 18e906200..6463021cf 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1271,7 +1271,7 @@ protected function getSQLType(string $type, int $size, bool $signed = true): str switch ($type) { case Database::VAR_STRING: // $size = $size * 4; // Convert utf8mb4 size to bytes - if ($size > 16383) { + if ($size > $this->getMaxVarcharLength()) { return 'TEXT'; } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 5b585b82b..97f364653 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -314,7 +314,7 @@ public function getAttributeWidth(Document $collection): int $total += 11; break; - case ($attribute['size'] > 16383): + case ($attribute['size'] > $this->getMaxVarcharLength()): // 8 bytes length + 2 bytes for TEXT $total += 10; break; @@ -865,4 +865,20 @@ public static function getPDOAttributes(): array PDO::ATTR_STRINGIFY_FETCHES => true // Returns all fetched data as Strings ]; } + + /** + * @return int + */ + public function getMaxVarcharLength(): int + { + return 16381; // Floor value for Postgres:16383 | MySQL:16381 | MariaDB:16382 + } + + /** + * @return int + */ + public function getMaxIndexLength(): int + { + return 768; + } } diff --git a/src/Database/Database.php b/src/Database/Database.php index 81bb24ade..a1f4faa5d 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -144,7 +144,7 @@ class Database * List of Internal Ids * @var array> */ - protected array $attributes = [ + protected static array $attributes = [ [ '$id' => '$id', 'type' => self::VAR_STRING, @@ -619,9 +619,11 @@ public function delete(string $name): bool * @param array $indexes * @param array $permissions * @param bool $documentSecurity - * * @return Document + * @throws DatabaseException * @throws DuplicateException + * @throws InvalidArgumentException + * @throws LimitException */ public function createCollection(string $id, array $attributes = [], array $indexes = [], array $permissions = null, bool $documentSecurity = true): Document { @@ -640,12 +642,6 @@ public function createCollection(string $id, array $attributes = [], array $inde throw new DuplicateException('Collection ' . $id . ' already exists'); } - $this->adapter->createCollection($id, $attributes, $indexes); - - if ($id === self::METADATA) { - return new Document($this->collection); - } - $collection = new Document([ '$id' => ID::custom($id), '$permissions' => $permissions, @@ -655,6 +651,17 @@ public function createCollection(string $id, array $attributes = [], array $inde 'documentSecurity' => $documentSecurity ]); + $validator = new IndexValidator($this->adapter->getMaxIndexLength()); + if (!$validator->isValid($collection)) { + throw new DatabaseException($validator->getDescription()); + } + + $this->adapter->createCollection($id, $attributes, $indexes); + + if ($id === self::METADATA) { + return new Document($this->collection); + } + // Check index limits, if given if ($indexes && $this->adapter->getCountOfIndexes($collection) > $this->adapter->getLimitForIndexes()) { throw new LimitException('Index limit of ' . $this->adapter->getLimitForIndexes() . ' exceeded. Cannot create collection.'); @@ -692,7 +699,10 @@ public function createCollection(string $id, array $attributes = [], array $inde * @param bool $documentSecurity * * @return Document - * @throws DuplicateException + * @throws InvalidArgumentException + * @throws ConflictException + * @throws DatabaseException + * @throws InvalidArgumentException */ public function updateCollection(string $id, array $permissions, bool $documentSecurity): Document { @@ -1976,6 +1986,7 @@ public function renameIndex(string $collection, string $old, string $new): bool * @throws DuplicateException * @throws LimitException * @throws StructureException + * @throws Exception */ public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths = [], array $orders = []): bool { @@ -1985,8 +1996,8 @@ public function createIndex(string $collection, string $id, string $type, array $collection = $this->silent(fn () => $this->getCollection($collection)); - $validator = new IndexValidator($collection); - if (!$validator->isValid(['type' => $type, 'attributes' => $attributes])) { + $validator = new IndexValidator($this->adapter->getMaxIndexLength()); + if (!$validator->isValid($collection)) { throw new DatabaseException($validator->getDescription()); } @@ -2018,7 +2029,7 @@ public function createIndex(string $collection, string $id, string $type, array break; case self::INDEX_FULLTEXT: - if (!$this->adapter->getSupportForUniqueIndex()) { + if (!$this->adapter->getSupportForFulltextIndex()) { throw new DatabaseException('Fulltext index is not supported'); } break; @@ -2027,8 +2038,6 @@ public function createIndex(string $collection, string $id, string $type, array throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT); } - $index = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes, $lengths, $orders); - $collection->setAttribute('indexes', new Document([ '$id' => ID::custom($id), 'key' => $id, @@ -2038,6 +2047,13 @@ public function createIndex(string $collection, string $id, string $type, array 'orders' => $orders, ]), Document::SET_TYPE_APPEND); + $validator = new IndexValidator($this->adapter->getMaxIndexLength()); + if (!$validator->isValid($collection)) { + throw new DatabaseException($validator->getDescription()); + } + + $index = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes, $lengths, $orders); + if ($collection->getId() !== self::METADATA) { $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection)); } @@ -4167,10 +4183,10 @@ public static function addFilter(string $name, callable $encode, callable $decod * @return array * @throws DatabaseException */ - public function getInternalAttributes(): array + public static function getInternalAttributes(): array { $attributes = []; - foreach ($this->attributes as $internal) { + foreach (self::$attributes as $internal) { $attributes[] = new Document($internal); } return $attributes; diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 0097990de..65a7c9de5 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -3,17 +3,26 @@ namespace Utopia\Database\Validator; use Utopia\Database\Database; +use Utopia\Database\Exception as DatabaseException; use Utopia\Validator; use Utopia\Database\Document; class Index extends Validator { - protected string $message = 'Invalid Index'; - protected Document $collection; + protected string $message = 'Invalid index'; + protected int $maxLength; - public function __construct(Document $collection) + /** + * @var array $attributes + */ + protected array $attributes = []; + + /** + * @param int $maxLength + */ + public function __construct(int $maxLength) { - $this->collection = $collection; + $this->maxLength = $maxLength; } /** @@ -25,39 +34,156 @@ public function getDescription(): string return $this->message; } + /** + * @param Document $collection + * @return bool + */ + public function checkEmptyIndexAttributes(Document $collection): bool + { + foreach ($collection->getAttribute('indexes', []) as $index) { + if (empty($index->getAttribute('attributes', []))) { + $this->message = 'No attributes provided for index'; + return false; + } + } + + return true; + } + + /** + * @param Document $collection + * @return bool + */ + public function checkDuplicatedAttributes(Document $collection): bool + { + foreach ($collection->getAttribute('indexes', []) as $index) { + $attributes = $index->getAttribute('attributes', []); + $orders = $index->getAttribute('orders', []); + $stack = []; + foreach ($attributes as $key => $attribute) { + $direction = $orders[$key] ?? 'asc'; + $value = strtolower($attribute . '|' . $direction); + if (in_array($value, $stack)) { + $this->message = 'Duplicate attributes provided'; + return false; + } + $stack[] = $value; + } + } + + return true; + } + + /** + * @param Document $collection + * @return bool + * @throws DatabaseException + */ + public function checkFulltextIndexNonString(Document $collection): bool + { + foreach ($collection->getAttribute('indexes', []) as $index) { + if ($index->getAttribute('type') === Database::INDEX_FULLTEXT) { + foreach ($index->getAttribute('attributes', []) as $attributeName) { + $attribute = $this->attributes[$attributeName] ?? new Document([]); + if ($attribute->getAttribute('type', '') !== Database::VAR_STRING) { + $this->message = 'Attribute "'.$attribute->getAttribute('key', $attribute->getAttribute('$id')).'" cannot be part of a FULLTEXT index, must be of type string'; + return false; + } + } + } + } + + return true; + } + + /** + * @param Document $collection + * @return bool + */ + public function checkIndexLength(Document $collection): bool + { + foreach ($collection->getAttribute('indexes', []) as $index) { + if ($index->getAttribute('type') === Database::INDEX_FULLTEXT) { + return true; + } + + $total = 0; + $lengths = $index->getAttribute('lengths', []); + + foreach ($index->getAttribute('attributes', []) as $attributePosition => $attributeName) { + $attribute = $this->attributes[$attributeName]; + + switch ($attribute->getAttribute('type')) { + case Database::VAR_STRING: + $attributeSize = $attribute->getAttribute('size', 0); + $indexLength = $lengths[$attributePosition] ?? $attributeSize; + break; + + case Database::VAR_FLOAT: + $attributeSize = 2; // 8 bytes / 4 mb4 + $indexLength = 2; + break; + + default: + $attributeSize = 1; // 4 bytes / 4 mb4 + $indexLength = 1; + break; + } + + if ($indexLength > $attributeSize) { + $this->message = 'Index length '.$indexLength.' is larger than the size for '.$attributeName.': '.$attributeSize.'"'; + return false; + } + + $total += $indexLength; + } + + if ($total > $this->maxLength && $this->maxLength > 0) { + $this->message = 'Index length is longer than the maximum: ' . $this->maxLength; + return false; + } + } + + return true; + } + /** * Is valid. * * Returns true index if valid. - * @param mixed $value as index options + * @param Document $value * @return bool + * @throws DatabaseException */ public function isValid($value): bool { - $indexType = $value['type']; - $indexAttributes = $value['attributes']; + foreach ($value->getAttribute('attributes', []) as $attribute) { + $this->attributes[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute; + } - if (empty($indexAttributes)) { - $this->message = 'Missing attributes'; + foreach (Database::getInternalAttributes() as $attribute) { + $this->attributes[$attribute->getAttribute('$id')] = $attribute; + } + + if (!$this->checkEmptyIndexAttributes($value)) { return false; } - if ($indexType === Database::INDEX_FULLTEXT) { - $collectionAttributes = $this->collection->getAttributes()['attributes']; - foreach ($collectionAttributes as $collectionAttribute) { - foreach ($indexAttributes as $indexAttribute) { - if ($indexAttribute === $collectionAttribute['key']) { - if ($collectionAttribute['type'] !== Database::VAR_STRING) { - $this->message = 'Attribute "'.$collectionAttribute['key'].'" cannot be part of a FULLTEXT index'; - return false; - } - } - } - } + if (!$this->checkDuplicatedAttributes($value)) { + return false; + } + + if (!$this->checkFulltextIndexNonString($value)) { + return false; + } + + if (!$this->checkIndexLength($value)) { + return false; } return true; } + /** * Is array * diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 17a2bbc05..648438a0a 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -5,6 +5,7 @@ use Exception; use InvalidArgumentException; use PHPUnit\Framework\TestCase; +use Throwable; use Utopia\Database\Adapter\SQL; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -21,6 +22,7 @@ use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DatetimeValidator; +use Utopia\Database\Validator\Index; use Utopia\Database\Validator\Query\Filter; use Utopia\Database\Validator\Structure; use Utopia\Validator\Range; @@ -76,6 +78,132 @@ public function testCreateExistsDelete(): void $this->assertEquals(true, static::getDatabase()->create()); } + /** + * @throws Exception|Throwable + */ + public function testIndexValidation(): void + { + $attributes = [ + new Document([ + '$id' => ID::custom('title1'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 700, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ]), + new Document([ + '$id' => ID::custom('title2'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 500, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ]), + ]; + + $indexes = [ + new Document([ + '$id' => ID::custom('index1'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['title1', 'title2'], + 'lengths' => [701,50], + 'orders' => [], + ]), + ]; + + $collection = new Document([ + '$id' => ID::custom('index_length'), + 'name' => 'test', + 'attributes' => $attributes, + 'indexes' => $indexes + ]); + + $validator = new Index(static::getDatabase()->getAdapter()->getMaxIndexLength()); + + $errorMessage = 'Index length 701 is larger than the size for title1: 700"'; + $this->assertFalse($validator->isValid($collection)); + $this->assertEquals($errorMessage, $validator->getDescription()); + + try { + static::getDatabase()->createCollection($collection->getId(), $attributes, $indexes); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertEquals($errorMessage, $e->getMessage()); + } + + $indexes = [ + new Document([ + '$id' => ID::custom('index1'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['title1', 'title2'], + 'lengths' => [700], // 700, 500 (length(title2)) + 'orders' => [], + ]), + ]; + + $collection->setAttribute('indexes', $indexes); + + if (static::getDatabase()->getAdapter()->getMaxIndexLength() > 0) { + $errorMessage = 'Index length is longer than the maximum: ' . static::getDatabase()->getAdapter()->getMaxIndexLength(); + $this->assertFalse($validator->isValid($collection)); + $this->assertEquals($errorMessage, $validator->getDescription()); + + try { + static::getDatabase()->createCollection($collection->getId(), $attributes, $indexes); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertEquals($errorMessage, $e->getMessage()); + } + } + + $attributes[] = new Document([ + '$id' => ID::custom('integer'), + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 10000, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ]); + + $indexes = [ + new Document([ + '$id' => ID::custom('index1'), + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['title1', 'integer'], + 'lengths' => [], + 'orders' => [], + ]), + ]; + + $collection = new Document([ + '$id' => ID::custom('index_length'), + 'name' => 'test', + 'attributes' => $attributes, + 'indexes' => $indexes + ]); + + $errorMessage = 'Attribute "integer" cannot be part of a FULLTEXT index, must be of type string'; + $this->assertFalse($validator->isValid($collection)); + $this->assertEquals($errorMessage, $validator->getDescription()); + + try { + static::getDatabase()->createCollection($collection->getId(), $attributes, $indexes); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertEquals($errorMessage, $e->getMessage()); + } + } + public function testCreatedAtUpdatedAt(): void { $this->assertInstanceOf('Utopia\Database\Document', static::getDatabase()->createCollection('created_at')); @@ -125,7 +253,7 @@ public function testCreateDeleteAttribute(): void static::getDatabase()->createCollection('attributes'); $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string1', Database::VAR_STRING, 128, true)); - $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string2', Database::VAR_STRING, 16383 + 1, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string2', Database::VAR_STRING, 16382 + 1, true)); $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string3', Database::VAR_STRING, 65535 + 1, true)); $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string4', Database::VAR_STRING, 16777215 + 1, true)); $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'integer', Database::VAR_INTEGER, 0, true)); @@ -778,7 +906,7 @@ public function testCreateDocumentDefaults(): void } /** - * @throws AuthorizationException|LimitException|DuplicateException|StructureException|Exception|\Throwable + * @throws AuthorizationException|LimitException|DuplicateException|StructureException|Exception|Throwable */ public function testIncreaseDecrease(): Document { @@ -901,7 +1029,13 @@ public function testGetDocumentSelect(Document $document): Document public function testFulltextIndexWithInteger(): void { $this->expectException(Exception::class); - $this->expectExceptionMessage('Attribute "integer" cannot be part of a FULLTEXT index'); + + if (!$this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { + $this->expectExceptionMessage('Fulltext index is not supported'); + } else { + $this->expectExceptionMessage('Attribute "integer" cannot be part of a FULLTEXT index, must be of type string'); + } + static::getDatabase()->createIndex('documents', 'fulltext_integer', Database::INDEX_FULLTEXT, ['string','integer']); } @@ -4198,7 +4332,7 @@ public function testOneToOneOneWayRelationship(): void * @throws LimitException * @throws DuplicateException * @throws StructureException - * @throws \Throwable + * @throws Throwable */ public function testOneToOneTwoWayRelationship(): void { diff --git a/tests/Database/Validator/IndexTest.php b/tests/Database/Validator/IndexTest.php new file mode 100644 index 000000000..ea6a349f4 --- /dev/null +++ b/tests/Database/Validator/IndexTest.php @@ -0,0 +1,221 @@ + ID::custom('test'), + 'name' => 'test', + 'attributes' => [ + new Document([ + '$id' => ID::custom('title'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 255, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ]), + new Document([ + '$id' => ID::custom('date'), + 'type' => Database::VAR_DATETIME, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['datetime'], + ]), + ], + 'indexes' => [ + new Document([ + '$id' => ID::custom('index1'), + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['title', 'date'], + 'lengths' => [], + 'orders' => [], + ]), + ], + ]); + + $this->assertFalse($validator->isValid($collection)); + $this->assertEquals('Attribute "date" cannot be part of a FULLTEXT index, must be of type string', $validator->getDescription()); + } + + /** + * @throws Exception + */ + public function testIndexLength(): void + { + $validator = new Index(768); + + $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' => ['title'], + 'lengths' => [], + 'orders' => [], + ]), + ], + ]); + + $this->assertFalse($validator->isValid($collection)); + $this->assertEquals('Index length is longer than the maximum: 768', $validator->getDescription()); + } + + /** + * @throws Exception + */ + public function testEmptyAttributes(): void + { + $validator = new Index(768); + + $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' => [], + 'lengths' => [], + 'orders' => [], + ]), + ], + ]); + + $this->assertFalse($validator->isValid($collection)); + $this->assertEquals('No attributes provided for index', $validator->getDescription()); + } + + /** + * @throws Exception + */ + public function testDuplicatedAttributes(): void + { + $validator = new Index(768); + + $collection = new Document([ + '$id' => ID::custom('test'), + 'name' => 'test', + 'attributes' => [ + new Document([ + '$id' => ID::custom('title'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 255, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ]) + ], + 'indexes' => [ + new Document([ + '$id' => ID::custom('index1'), + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['title', 'title'], + 'lengths' => [], + 'orders' => [], + ]), + ], + ]); + + $this->assertFalse($validator->isValid($collection)); + $this->assertEquals('Duplicate attributes provided', $validator->getDescription()); + } + + /** + * @throws Exception + */ + public function testDuplicatedAttributesDifferentOrder(): void + { + $validator = new Index(768); + + $collection = new Document([ + '$id' => ID::custom('test'), + 'name' => 'test', + 'attributes' => [ + new Document([ + '$id' => ID::custom('title'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 255, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ]) + ], + 'indexes' => [ + new Document([ + '$id' => ID::custom('index1'), + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['title', 'title'], + 'lengths' => [], + 'orders' => ['asc', 'desc'], + ]), + ], + ]); + + $this->assertTrue($validator->isValid($collection)); + } +}