From 6f578b30345e523922886a8b015edea1b3f875eb Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 9 Oct 2023 13:55:30 +1300 Subject: [PATCH 1/4] Update index validation --- src/Database/Database.php | 30 +++-- src/Database/Validator/Index.php | 164 +++++++++++-------------- tests/Database/Base.php | 9 +- tests/Database/Validator/IndexTest.php | 49 ++++---- 4 files changed, 122 insertions(+), 130 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 8292bf6bf..c530af9d5 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -690,9 +690,14 @@ 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()); + $validator = new IndexValidator( + $attributes, + $this->adapter->getMaxIndexLength() + ); + foreach ($indexes as $index) { + if (!$validator->isValid($index)) { + throw new DatabaseException($validator->getDescription()); + } } $this->adapter->createCollection($id, $attributes, $indexes); @@ -2047,11 +2052,6 @@ public function createIndex(string $collection, string $id, string $type, array $collection = $this->silent(fn () => $this->getCollection($collection)); - $validator = new IndexValidator($this->adapter->getMaxIndexLength()); - if (!$validator->isValid($collection)) { - throw new DatabaseException($validator->getDescription()); - } - // index IDs are case-insensitive $indexes = $collection->getAttribute('indexes', []); @@ -2089,17 +2089,23 @@ 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); } - $collection->setAttribute('indexes', new Document([ + $index = new Document([ '$id' => ID::custom($id), 'key' => $id, 'type' => $type, 'attributes' => $attributes, 'lengths' => $lengths, 'orders' => $orders, - ]), Document::SET_TYPE_APPEND); + ]); + + $collection->setAttribute('indexes', $index, Document::SET_TYPE_APPEND); + + $validator = new IndexValidator( + $collection->getAttribute('attributes', []), + $this->adapter->getMaxIndexLength() + ); - $validator = new IndexValidator($this->adapter->getMaxIndexLength()); - if (!$validator->isValid($collection)) { + if (!$validator->isValid($index)) { throw new DatabaseException($validator->getDescription()); } diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 4ebc4f98a..eff1ea419 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -15,14 +15,22 @@ class Index extends Validator /** * @var array $attributes */ - protected array $attributes = []; + protected array $attributes; /** + * @param array $attributes * @param int $maxLength */ - public function __construct(int $maxLength) + public function __construct(array $attributes, int $maxLength) { $this->maxLength = $maxLength; + + foreach ($attributes as $attribute) { + $this->attributes[$attribute->getAttribute('$id')] = $attribute; + } + foreach (Database::getInternalAttributes() as $attribute) { + $this->attributes[$attribute->getAttribute('$id')] = $attribute; + } } /** @@ -35,131 +43,115 @@ public function getDescription(): string } /** - * @param Document $collection + * @param Document $index * @return bool */ - public function checkAttributesNotFound(Document $collection): bool + public function checkAttributesNotFound(Document $index): bool { - foreach ($collection->getAttribute('indexes', []) as $index) { - foreach ($index->getAttribute('attributes', []) as $attributeName) { - if (!isset($this->attributes[$attributeName])) { - $this->message = 'Invalid index attribute "' . $attributeName . '" not found'; - return false; - } + foreach ($index->getAttribute('attributes', []) as $attribute) { + if (!isset($this->attributes[$attribute])) { + $this->message = 'Invalid index attribute "' . $attribute . '" not found'; + return false; } } - return true; } /** - * @param Document $collection + * @param Document $index * @return bool */ - public function checkEmptyIndexAttributes(Document $collection): bool + public function checkEmptyIndexAttributes(Document $index): bool { - foreach ($collection->getAttribute('indexes', []) as $index) { - if (empty($index->getAttribute('attributes', []))) { - $this->message = 'No attributes provided for index'; - return false; - } + if (empty($index->getAttribute('attributes', []))) { + $this->message = 'No attributes provided for index'; + return false; } - return true; } /** - * @param Document $collection + * @param Document $index * @return bool */ - public function checkDuplicatedAttributes(Document $collection): bool + public function checkDuplicatedAttributes(Document $index): 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; + $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 + * @param Document $index * @return bool * @throws DatabaseException */ - public function checkFulltextIndexNonString(Document $collection): bool + public function checkFulltextIndexNonString(Document $index): 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; - } + if ($index->getAttribute('type') === Database::INDEX_FULLTEXT) { + foreach ($index->getAttribute('attributes', []) as $attribute) { + $attribute = $this->attributes[$attribute] ?? 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 + * @param Document $index * @return bool */ - public function checkIndexLength(Document $collection): bool + public function checkIndexLength(Document $index): bool { - foreach ($collection->getAttribute('indexes', []) as $index) { - if ($index->getAttribute('type') === Database::INDEX_FULLTEXT) { - continue; - } - - $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; - } + if ($index->getAttribute('type') === Database::INDEX_FULLTEXT) { + return true; + } - $total += $indexLength; + $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 ($total > $this->maxLength && $this->maxLength > 0) { - $this->message = 'Index length is longer than the maximum: ' . $this->maxLength; + 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; @@ -175,14 +167,6 @@ public function checkIndexLength(Document $collection): bool */ public function isValid($value): bool { - foreach ($value->getAttribute('attributes', []) as $attribute) { - $this->attributes[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute; - } - - foreach (Database::getInternalAttributes() as $attribute) { - $this->attributes[$attribute->getAttribute('$id')] = $attribute; - } - if (!$this->checkAttributesNotFound($value)) { return false; } diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 34609d113..afb537ee9 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -122,10 +122,10 @@ public function testIndexValidation(): void 'indexes' => $indexes ]); - $validator = new Index(static::getDatabase()->getAdapter()->getMaxIndexLength()); + $validator = new Index($attributes, static::getDatabase()->getAdapter()->getMaxIndexLength()); $errorMessage = 'Index length 701 is larger than the size for title1: 700"'; - $this->assertFalse($validator->isValid($collection)); + $this->assertFalse($validator->isValid($indexes[0])); $this->assertEquals($errorMessage, $validator->getDescription()); try { @@ -149,7 +149,7 @@ public function testIndexValidation(): void if (static::getDatabase()->getAdapter()->getMaxIndexLength() > 0) { $errorMessage = 'Index length is longer than the maximum: ' . static::getDatabase()->getAdapter()->getMaxIndexLength(); - $this->assertFalse($validator->isValid($collection)); + $this->assertFalse($validator->isValid($indexes[0])); $this->assertEquals($errorMessage, $validator->getDescription()); try { @@ -189,8 +189,9 @@ public function testIndexValidation(): void 'indexes' => $indexes ]); + $validator = new Index($attributes, static::getDatabase()->getAdapter()->getMaxIndexLength()); $errorMessage = 'Attribute "integer" cannot be part of a FULLTEXT index, must be of type string'; - $this->assertFalse($validator->isValid($collection)); + $this->assertFalse($validator->isValid($indexes[0])); $this->assertEquals($errorMessage, $validator->getDescription()); try { diff --git a/tests/Database/Validator/IndexTest.php b/tests/Database/Validator/IndexTest.php index 4247ecd32..8f3aa70fb 100644 --- a/tests/Database/Validator/IndexTest.php +++ b/tests/Database/Validator/IndexTest.php @@ -24,8 +24,6 @@ public function tearDown(): void */ public function testAttributeNotFound(): void { - $validator = new Index(768); - $collection = new Document([ '$id' => ID::custom('test'), 'name' => 'test', @@ -53,7 +51,9 @@ public function testAttributeNotFound(): void ], ]); - $this->assertFalse($validator->isValid($collection)); + $validator = new Index($collection->getAttribute('attributes'), 768); + $index = $collection->getAttribute('indexes')[0]; + $this->assertFalse($validator->isValid($index)); $this->assertEquals('Invalid index attribute "not_exist" not found', $validator->getDescription()); } @@ -62,8 +62,6 @@ public function testAttributeNotFound(): void */ public function testFulltextWithNonString(): void { - $validator = new Index(768); - $collection = new Document([ '$id' => ID::custom('test'), 'name' => 'test', @@ -102,7 +100,9 @@ public function testFulltextWithNonString(): void ], ]); - $this->assertFalse($validator->isValid($collection)); + $validator = new Index($collection->getAttribute('attributes'), 768); + $index = $collection->getAttribute('indexes')[0]; + $this->assertFalse($validator->isValid($index)); $this->assertEquals('Attribute "date" cannot be part of a FULLTEXT index, must be of type string', $validator->getDescription()); } @@ -111,8 +111,6 @@ public function testFulltextWithNonString(): void */ public function testIndexLength(): void { - $validator = new Index(768); - $collection = new Document([ '$id' => ID::custom('test'), 'name' => 'test', @@ -140,7 +138,9 @@ public function testIndexLength(): void ], ]); - $this->assertFalse($validator->isValid($collection)); + $validator = new Index($collection->getAttribute('attributes'), 768); + $index = $collection->getAttribute('indexes')[0]; + $this->assertFalse($validator->isValid($index)); $this->assertEquals('Index length is longer than the maximum: 768', $validator->getDescription()); } @@ -149,8 +149,6 @@ public function testIndexLength(): void */ public function testMultipleIndexLength(): void { - $validator = new Index(768); - $collection = new Document([ '$id' => ID::custom('test'), 'name' => 'test', @@ -187,15 +185,18 @@ public function testMultipleIndexLength(): void ], ]); - $this->assertTrue($validator->isValid($collection)); + $validator = new Index($collection->getAttribute('attributes'), 768); + $index = $collection->getAttribute('indexes')[0]; + $this->assertTrue($validator->isValid($index)); - $collection->setAttribute('indexes', new Document([ + $index = new Document([ '$id' => ID::custom('index2'), 'type' => Database::INDEX_KEY, 'attributes' => ['title', 'description'], - ]), Document::SET_TYPE_APPEND); + ]); - $this->assertFalse($validator->isValid($collection)); + $collection->setAttribute('indexes', $index, Document::SET_TYPE_APPEND); + $this->assertFalse($validator->isValid($index)); $this->assertEquals('Index length is longer than the maximum: 768', $validator->getDescription()); } @@ -204,8 +205,6 @@ public function testMultipleIndexLength(): void */ public function testEmptyAttributes(): void { - $validator = new Index(768); - $collection = new Document([ '$id' => ID::custom('test'), 'name' => 'test', @@ -233,7 +232,9 @@ public function testEmptyAttributes(): void ], ]); - $this->assertFalse($validator->isValid($collection)); + $validator = new Index($collection->getAttribute('attributes'), 768); + $index = $collection->getAttribute('indexes')[0]; + $this->assertFalse($validator->isValid($index)); $this->assertEquals('No attributes provided for index', $validator->getDescription()); } @@ -242,8 +243,6 @@ public function testEmptyAttributes(): void */ public function testDuplicatedAttributes(): void { - $validator = new Index(768); - $collection = new Document([ '$id' => ID::custom('test'), 'name' => 'test', @@ -271,7 +270,9 @@ public function testDuplicatedAttributes(): void ], ]); - $this->assertFalse($validator->isValid($collection)); + $validator = new Index($collection->getAttribute('attributes'), 768); + $index = $collection->getAttribute('indexes')[0]; + $this->assertFalse($validator->isValid($index)); $this->assertEquals('Duplicate attributes provided', $validator->getDescription()); } @@ -280,8 +281,6 @@ public function testDuplicatedAttributes(): void */ public function testDuplicatedAttributesDifferentOrder(): void { - $validator = new Index(768); - $collection = new Document([ '$id' => ID::custom('test'), 'name' => 'test', @@ -309,6 +308,8 @@ public function testDuplicatedAttributesDifferentOrder(): void ], ]); - $this->assertTrue($validator->isValid($collection)); + $validator = new Index($collection->getAttribute('attributes'), 768); + $index = $collection->getAttribute('indexes')[0]; + $this->assertTrue($validator->isValid($index)); } } From 95cc7a89cb8889623448986a501ee39cbc209739 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 9 Oct 2023 14:08:24 +1300 Subject: [PATCH 2/4] Fix key match --- src/Database/Validator/Index.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index eff1ea419..f326c0d70 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -26,10 +26,12 @@ public function __construct(array $attributes, int $maxLength) $this->maxLength = $maxLength; foreach ($attributes as $attribute) { - $this->attributes[$attribute->getAttribute('$id')] = $attribute; + $key = $attribute->getAttribute('key', $attribute->getAttribute('$id')); + $this->attributes[$key] = $attribute; } foreach (Database::getInternalAttributes() as $attribute) { - $this->attributes[$attribute->getAttribute('$id')] = $attribute; + $key = $attribute->getAttribute('$id'); + $this->attributes[$key] = $attribute; } } From 69024a1cd9cb17bddce03d23d43374ccba346233 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 9 Oct 2023 15:03:06 +1300 Subject: [PATCH 3/4] Lowercase compare attribute keys --- src/Database/Validator/Index.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index f326c0d70..6ae7af5c7 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -26,11 +26,11 @@ public function __construct(array $attributes, int $maxLength) $this->maxLength = $maxLength; foreach ($attributes as $attribute) { - $key = $attribute->getAttribute('key', $attribute->getAttribute('$id')); + $key = \strtolower($attribute->getAttribute('key', $attribute->getAttribute('$id'))); $this->attributes[$key] = $attribute; } foreach (Database::getInternalAttributes() as $attribute) { - $key = $attribute->getAttribute('$id'); + $key = \strtolower($attribute->getAttribute('$id')); $this->attributes[$key] = $attribute; } } @@ -51,7 +51,7 @@ public function getDescription(): string public function checkAttributesNotFound(Document $index): bool { foreach ($index->getAttribute('attributes', []) as $attribute) { - if (!isset($this->attributes[$attribute])) { + if (!isset($this->attributes[\strtolower($attribute)])) { $this->message = 'Invalid index attribute "' . $attribute . '" not found'; return false; } From 67adc586318c35b34aaa9f1eed8f3751b6e8c324 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 9 Oct 2023 15:11:28 +1300 Subject: [PATCH 4/4] Fix other case checks --- src/Database/Validator/Index.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 6ae7af5c7..a0b6b80f5 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -102,7 +102,7 @@ public function checkFulltextIndexNonString(Document $index): bool { if ($index->getAttribute('type') === Database::INDEX_FULLTEXT) { foreach ($index->getAttribute('attributes', []) as $attribute) { - $attribute = $this->attributes[$attribute] ?? new Document(); + $attribute = $this->attributes[\strtolower($attribute)] ?? 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; @@ -126,7 +126,7 @@ public function checkIndexLength(Document $index): bool $lengths = $index->getAttribute('lengths', []); foreach ($index->getAttribute('attributes', []) as $attributePosition => $attributeName) { - $attribute = $this->attributes[$attributeName]; + $attribute = $this->attributes[\strtolower($attributeName)]; switch ($attribute->getAttribute('type')) { case Database::VAR_STRING: