From fdbbb76e11b493fe5a471aabf59d9b430930baee Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 9 Oct 2023 20:24:07 +1300 Subject: [PATCH 01/52] Add array contains support for SQL adapters --- src/Database/Adapter/MariaDB.php | 19 ++++++++---- src/Database/Adapter/Postgres.php | 15 +++++++--- src/Database/Adapter/SQL.php | 6 ++-- tests/Database/Base.php | 29 ++++++++++--------- tests/Database/QueryTest.php | 16 ---------- tests/Database/Validator/Query/FilterTest.php | 1 + 6 files changed, 44 insertions(+), 42 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index d4647f754..79521f50a 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -218,7 +218,7 @@ public function createAttribute(string $collection, string $id, string $type, in $type = $this->getSQLType($type, $size, $signed); if ($array) { - $type = 'LONGTEXT'; + $type = 'JSON'; } return $this->getPDO() @@ -246,7 +246,7 @@ public function updateAttribute(string $collection, string $id, string $type, in $type = $this->getSQLType($type, $size, $signed); if ($array) { - $type = 'LONGTEXT'; + $type = 'JSON'; } return $this->getPDO() @@ -1314,19 +1314,26 @@ protected function getSQLCondition(Query $query): string switch ($query->getMethod()) { case Query::TYPE_SEARCH: - return "MATCH(table_main.{$attribute}) AGAINST (:{$placeholder}_0 IN BOOLEAN MODE)"; + return "MATCH(`table_main`.{$attribute}) AGAINST (:{$placeholder}_0 IN BOOLEAN MODE)"; case Query::TYPE_BETWEEN: - return "table_main.{$attribute} BETWEEN :{$placeholder}_0 AND :{$placeholder}_1"; + return "`table_main`.{$attribute} BETWEEN :{$placeholder}_0 AND :{$placeholder}_1"; case Query::TYPE_IS_NULL: case Query::TYPE_IS_NOT_NULL: - return "table_main.{$attribute} {$this->getSQLOperator($query->getMethod())}"; + return "`table_main`.{$attribute} {$this->getSQLOperator($query->getMethod())}"; + case Query::TYPE_CONTAINS: + $conditions = []; + foreach ($query->getValues() as $key => $value) { + $conditions[] = "JSON_CONTAINS(`table_main`.{$attribute}, :{$placeholder}_{$key})";; + } + $condition = implode(' OR ', $conditions); + return empty($condition) ? '' : '(' . $condition . ')'; default: $conditions = []; foreach ($query->getValues() as $key => $value) { - $conditions[] = $attribute . ' ' . $this->getSQLOperator($query->getMethod()) . ' :' . $placeholder . '_' . $key; + $conditions[] = "{$attribute} {$this->getSQLOperator($query->getMethod())} :{$placeholder}_{$key}"; } $condition = implode(' OR ', $conditions); return empty($condition) ? '' : '(' . $condition . ')'; diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 8df026bda..3d4579ce8 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -219,7 +219,7 @@ public function createAttribute(string $collection, string $id, string $type, in $type = $this->getSQLType($type, $size, $signed); if ($array) { - $type = 'TEXT'; + $type = 'JSONB'; } return $this->getPDO() @@ -292,7 +292,7 @@ public function updateAttribute(string $collection, string $id, string $type, in $type = $this->getSQLType($type, $size, $signed); if ($array) { - $type = 'LONGTEXT'; + $type = 'JSONB'; } if ($type == 'TIMESTAMP(3)') { @@ -1045,7 +1045,6 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $where[] = $this->getSQLCondition($query); } - if (Authorization::$status) { $where[] = $this->getSQLPermissionsCondition($name, $roles); } @@ -1321,7 +1320,7 @@ protected function getSQLCondition(Query $query): string default => $query->getAttribute() }); - $attribute = "\"{$query->getAttribute()}\"" ; + $attribute = "\"{$query->getAttribute()}\""; $placeholder = $this->getSQLPlaceholder($query); switch ($query->getMethod()) { @@ -1335,6 +1334,14 @@ protected function getSQLCondition(Query $query): string case Query::TYPE_IS_NOT_NULL: return "table_main.{$attribute} {$this->getSQLOperator($query->getMethod())}"; + case Query::TYPE_CONTAINS: + $conditions = []; + foreach ($query->getValues() as $key => $value) { + $conditions[] = "{$attribute} @> :{$placeholder}_{$key}"; + } + $condition = implode(' OR ', $conditions); + return empty($condition) ? '' : '(' . $condition . ')'; + default: $conditions = []; foreach ($query->getValues() as $key => $value) { diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 8525d2738..f3e0fbca0 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -678,7 +678,7 @@ public function getSupportForCasting(): bool */ public function getSupportForQueryContains(): bool { - return false; + return true; } public function getSupportForRelationships(): bool @@ -703,10 +703,12 @@ protected function bindConditionValue(mixed $stmt, Query $query): void Query::TYPE_STARTS_WITH => $this->escapeWildcards($value) . '%', Query::TYPE_ENDS_WITH => '%' . $this->escapeWildcards($value), Query::TYPE_SEARCH => $this->getFulltextValue($value), + Query::TYPE_CONTAINS => \json_encode($value), default => $value }; - $placeholder = $this->getSQLPlaceholder($query).'_'.$key; + $placeholder = $this->getSQLPlaceholder($query) . '_' . $key; + $stmt->bindValue($placeholder, $value, $this->getPDOType($value)); } } diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 34609d113..a595bc006 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -1483,17 +1483,18 @@ public function testDeleteDocument(Document $document): void public function testFind(): array { Authorization::setRole(Role::any()->toString()); + static::getDatabase()->createCollection('movies', permissions: [ Permission::create(Role::any()), Permission::update(Role::users()) - ], documentSecurity: true); + ]); $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'name', Database::VAR_STRING, 128, true)); $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'director', Database::VAR_STRING, 128, true)); $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'year', Database::VAR_INTEGER, 0, true)); $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'price', Database::VAR_FLOAT, 0, true)); $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'active', Database::VAR_BOOLEAN, 0, true)); - $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'generes', Database::VAR_STRING, 32, true, null, true, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'genres', Database::VAR_STRING, 32, true, null, true, true)); $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'with-dash', Database::VAR_STRING, 128, true)); $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'nullable', Database::VAR_STRING, 128, false)); @@ -1518,7 +1519,7 @@ public function testFind(): array 'year' => 2013, 'price' => 39.50, 'active' => true, - 'generes' => ['animation', 'kids'], + 'genres' => ['animation', 'kids'], 'with-dash' => 'Works' ])); @@ -1542,7 +1543,7 @@ public function testFind(): array 'year' => 2019, 'price' => 39.50, 'active' => true, - 'generes' => ['animation', 'kids'], + 'genres' => ['animation', 'kids'], 'with-dash' => 'Works' ])); @@ -1566,7 +1567,7 @@ public function testFind(): array 'year' => 2011, 'price' => 25.94, 'active' => true, - 'generes' => ['science fiction', 'action', 'comics'], + 'genres' => ['science fiction', 'action', 'comics'], 'with-dash' => 'Works2' ])); @@ -1590,7 +1591,7 @@ public function testFind(): array 'year' => 2019, 'price' => 25.99, 'active' => true, - 'generes' => ['science fiction', 'action', 'comics'], + 'genres' => ['science fiction', 'action', 'comics'], 'with-dash' => 'Works2' ])); @@ -1614,7 +1615,7 @@ public function testFind(): array 'year' => 2025, 'price' => 0.0, 'active' => false, - 'generes' => [], + 'genres' => [], 'with-dash' => 'Works3' ])); @@ -1636,7 +1637,7 @@ public function testFind(): array 'year' => 2026, 'price' => 0.0, 'active' => false, - 'generes' => [], + 'genres' => [], 'with-dash' => 'Works3', 'nullable' => 'Not null' ])); @@ -1665,8 +1666,8 @@ public function testFindBasicChecks(): void $this->assertIsFloat($documents[0]->getAttribute('price')); $this->assertEquals(true, $documents[0]->getAttribute('active')); $this->assertIsBool($documents[0]->getAttribute('active')); - $this->assertEquals(['animation', 'kids'], $documents[0]->getAttribute('generes')); - $this->assertIsArray($documents[0]->getAttribute('generes')); + $this->assertEquals(['animation', 'kids'], $documents[0]->getAttribute('genres')); + $this->assertIsArray($documents[0]->getAttribute('genres')); $this->assertEquals('Works', $documents[0]->getAttribute('with-dash')); // Alphabetical order @@ -1836,7 +1837,7 @@ public function testFindContains(): void } $documents = static::getDatabase()->find('movies', [ - Query::contains('generes', ['comics']) + Query::contains('genres', ['comics']) ]); $this->assertEquals(2, count($documents)); @@ -1845,7 +1846,7 @@ public function testFindContains(): void * Array contains OR condition */ $documents = static::getDatabase()->find('movies', [ - Query::contains('generes', ['comics', 'kids']), + Query::contains('genres', ['comics', 'kids']), ]); $this->assertEquals(4, count($documents)); @@ -3786,7 +3787,7 @@ public function testUniqueIndexDuplicate(): void 'year' => 2013, 'price' => 39.50, 'active' => true, - 'generes' => ['animation', 'kids'], + 'genres' => ['animation', 'kids'], 'with-dash' => 'Works4' ])); } @@ -3818,7 +3819,7 @@ public function testUniqueIndexDuplicateUpdate(): void 'year' => 2013, 'price' => 39.50, 'active' => true, - 'generes' => ['animation', 'kids'], + 'genres' => ['animation', 'kids'], 'with-dash' => 'Works4' ])); diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index a7e497959..fc57bfea4 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -375,22 +375,6 @@ public function testParseV2(): void $this->assertEquals('"quoted":"colon"', $query->getValue()); } - /* - Tests for aliases if we enable them: - public function testAlias(): void { - $query = Query::parse('eq(1)'); - $this->assertEquals(Query::TYPE_EQUAL, $query->getMethod()); - $query = Query::parse('lt(1)'); - $this->assertEquals(Query::TYPE_LESSER, $query->getMethod()); - $query = Query::parse('lte(1)'); - $this->assertEquals(Query::TYPE_LESSEREQUAL, $query->getMethod()); - $query = Query::parse('gt(1)'); - $this->assertEquals(Query::TYPE_GREATER, $query->getMethod()); - $query = Query::parse('gte(1)'); - $this->assertEquals(Query::TYPE_GREATEREQUAL, $query->getMethod()); - } - */ - public function testParseComplex(): void { $queries = [ diff --git a/tests/Database/Validator/Query/FilterTest.php b/tests/Database/Validator/Query/FilterTest.php index 752dd97a3..1a948d6db 100644 --- a/tests/Database/Validator/Query/FilterTest.php +++ b/tests/Database/Validator/Query/FilterTest.php @@ -34,6 +34,7 @@ public function testSuccess(): void $this->assertTrue($this->validator->isValid(Query::isNull('attr'))); $this->assertTrue($this->validator->isValid(Query::startsWith('attr', 'super'))); $this->assertTrue($this->validator->isValid(Query::endsWith('attr', 'man'))); + $this->assertTrue($this->validator->isValid(Query::contains('attr', ['super']))); } public function testFailure(): void From fc6b19826686ceca9091220abf3ce9b0e34948a7 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 9 Oct 2023 20:43:28 +1300 Subject: [PATCH 02/52] Remove dupe semicolon --- src/Database/Adapter/MariaDB.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 79521f50a..7f772d719 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1326,7 +1326,7 @@ protected function getSQLCondition(Query $query): string case Query::TYPE_CONTAINS: $conditions = []; foreach ($query->getValues() as $key => $value) { - $conditions[] = "JSON_CONTAINS(`table_main`.{$attribute}, :{$placeholder}_{$key})";; + $conditions[] = "JSON_CONTAINS(`table_main`.{$attribute}, :{$placeholder}_{$key})"; } $condition = implode(' OR ', $conditions); return empty($condition) ? '' : '(' . $condition . ')'; From e8295c1b94f306caa18630f985e90997fca07d12 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 9 Oct 2023 20:45:33 +1300 Subject: [PATCH 03/52] Disable SQLite array contains --- src/Database/Adapter/SQLite.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 932de4f3f..269134093 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -657,6 +657,11 @@ public function getSupportForSchemas(): bool return false; } + public function getSupportForQueryContains(): bool + { + return false; + } + /** * Is fulltext index supported? * From 2985269318f0e76dc12510f488c31f1b1b8a2f6d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 13 Oct 2023 12:35:07 +1300 Subject: [PATCH 04/52] Disallow contains queries on non-array attributes --- src/Database/Database.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Database/Database.php b/src/Database/Database.php index 8292bf6bf..39b0bb1ef 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4108,6 +4108,16 @@ public function find(string $collection, array $queries = [], ?int $timeout = nu foreach ($queries as $index => &$query) { switch ($query->getMethod()) { + case Query::TYPE_CONTAINS: + $attribute = $query->getAttribute(); + foreach ($collection->getAttribute('attributes', []) as $attr) { + $key = $attr->getAttribute('key', $attr->getAttribute('$id')); + $array = $attr->getAttribute('array', false); + if ($key === $attribute && !$array) { + throw new DatabaseException('Cannot query contains on attribute "' . $attribute . '" because it is not an array.'); + } + } + break; case Query::TYPE_SELECT: $values = $query->getValues(); foreach ($values as $valueIndex => $value) { From 15ba9b18808828535e1c0c3a03f77914af58e768 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 13 Oct 2023 12:40:41 +1300 Subject: [PATCH 05/52] Add test for non-array attribute --- tests/Database/Base.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Database/Base.php b/tests/Database/Base.php index a595bc006..4786d050a 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -10,6 +10,7 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; +use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Authorization as AuthorizationException; use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Duplicate as DuplicateException; @@ -1850,6 +1851,18 @@ public function testFindContains(): void ]); $this->assertEquals(4, count($documents)); + + $documents = static::getDatabase()->find('movies', [ + Query::contains('genres', ['non-existent']), + ]); + + $this->assertEquals(0, count($documents)); + + $this->expectException(DatabaseException::class); + + static::getDatabase()->find('movies', [ + Query::contains('name', ['Frozen']), + ]); } public function testFindFulltext(): void From 712200642ecbf51a0ee8750ed8aa548b27cab7c8 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 20 Oct 2023 16:57:39 +1300 Subject: [PATCH 06/52] Commit benchmark results for comparisons --- .gitignore | 3 -- ...adb_myapp_642ba64fe5e9f_25_1680582832.json | 1 + ...adb_myapp_642baa6d33383_25_1680583630.json | 47 +++++++++++++++++ .../mariadb_testing_1000_1697771040.json | 52 +++++++++++++++++++ .../mariadb_testing_1000_1697773496.json | 47 +++++++++++++++++ .../mariadb_testing_1000_1697773900.json | 52 +++++++++++++++++++ .../mariadb_testing_1000_1697773977.json | 1 + 7 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 bin/view/results/mariadb_myapp_642ba64fe5e9f_25_1680582832.json create mode 100644 bin/view/results/mariadb_myapp_642baa6d33383_25_1680583630.json create mode 100644 bin/view/results/mariadb_testing_1000_1697771040.json create mode 100644 bin/view/results/mariadb_testing_1000_1697773496.json create mode 100644 bin/view/results/mariadb_testing_1000_1697773900.json create mode 100644 bin/view/results/mariadb_testing_1000_1697773977.json diff --git a/.gitignore b/.gitignore index 8f1497051..46daf3d31 100755 --- a/.gitignore +++ b/.gitignore @@ -5,12 +5,9 @@ mock.json data-tests.php loader.php .phpunit.result.cache -/bin/view/results/ .vscode .vscode/* database.sql - -## - Oh Wess! Makefile .envrc .vscode diff --git a/bin/view/results/mariadb_myapp_642ba64fe5e9f_25_1680582832.json b/bin/view/results/mariadb_myapp_642ba64fe5e9f_25_1680582832.json new file mode 100644 index 000000000..a465c3af4 --- /dev/null +++ b/bin/view/results/mariadb_myapp_642ba64fe5e9f_25_1680582832.json @@ -0,0 +1 @@ +[{"roles":2,"results":[0.004142045974731445,0.0007698535919189453,0.0006570816040039062,4.3037660121917725]},{"roles":102,"results":[0.0031499862670898438,0.0012857913970947266,0.0013210773468017578,4.130218029022217]},{"roles":491,"results":[0.4965331554412842,0.36292195320129395,0.30788612365722656,4.9501330852508545]},{"roles":964,"results":[0.44646310806274414,0.44009995460510254,0.37430691719055176,4.239892959594727]},{"roles":1829,"results":[0.6837189197540283,1.6691820621490479,1.3487520217895508,98.95817399024963]}] \ No newline at end of file diff --git a/bin/view/results/mariadb_myapp_642baa6d33383_25_1680583630.json b/bin/view/results/mariadb_myapp_642baa6d33383_25_1680583630.json new file mode 100644 index 000000000..6cbfa187b --- /dev/null +++ b/bin/view/results/mariadb_myapp_642baa6d33383_25_1680583630.json @@ -0,0 +1,47 @@ +[ + { + "roles": 2, + "results": [ + 0.004247903823852539, + 0.0007619857788085938, + 0.0008020401000976562, + 4.970219850540161 + ] + }, + { + "roles": 101, + "results": [ + 0.0033349990844726562, + 0.001294851303100586, + 0.001383066177368164, + 4.7076640129089355 + ] + }, + { + "roles": 485, + "results": [ + 0.4268150329589844, + 0.32375311851501465, + 0.35645008087158203, + 4.3803019523620605 + ] + }, + { + "roles": 946, + "results": [ + 0.4152810573577881, + 0.4178469181060791, + 0.4439430236816406, + 4.6542909145355225 + ] + }, + { + "roles": 1809, + "results": [ + 0.7865281105041504, + 1.7059669494628906, + 1.4522700309753418, + 98.20597195625305 + ] + } +] \ No newline at end of file diff --git a/bin/view/results/mariadb_testing_1000_1697771040.json b/bin/view/results/mariadb_testing_1000_1697771040.json new file mode 100644 index 000000000..d08e1ca68 --- /dev/null +++ b/bin/view/results/mariadb_testing_1000_1697771040.json @@ -0,0 +1,52 @@ +[ + { + "roles": 2, + "results": { + "greaterThan, equal[1], limit(1000)": 0.014531135559082031, + "equal[3], limit(1000)": 0.01854395866394043, + "greaterThan, limit(1000)": 0.03291916847229004, + "search, limit(1000)": 0.037921905517578125, + "contains[1], limit(1000)": 0.0019600391387939453 + } + }, + { + "roles": 102, + "results": { + "greaterThan, equal[1], limit(1000)": 0.0018169879913330078, + "equal[3], limit(1000)": 0.012106895446777344, + "greaterThan, limit(1000)": 0.030903100967407227, + "search, limit(1000)": 0.03960585594177246, + "contains[1], limit(1000)": 0.0017769336700439453 + } + }, + { + "roles": 495, + "results": { + "greaterThan, equal[1], limit(1000)": 0.0020799636840820312, + "equal[3], limit(1000)": 0.013564109802246094, + "greaterThan, limit(1000)": 0.032176971435546875, + "search, limit(1000)": 0.03503084182739258, + "contains[1], limit(1000)": 0.001474142074584961 + } + }, + { + "roles": 954, + "results": { + "greaterThan, equal[1], limit(1000)": 0.0017261505126953125, + "equal[3], limit(1000)": 0.012243986129760742, + "greaterThan, limit(1000)": 0.03142595291137695, + "search, limit(1000)": 0.03658008575439453, + "contains[1], limit(1000)": 0.0016679763793945312 + } + }, + { + "roles": 1812, + "results": { + "greaterThan, equal[1], limit(1000)": 0.0019099712371826172, + "equal[3], limit(1000)": 0.012614965438842773, + "greaterThan, limit(1000)": 0.030133962631225586, + "search, limit(1000)": 0.03749680519104004, + "contains[1], limit(1000)": 0.0017859935760498047 + } + } +] \ No newline at end of file diff --git a/bin/view/results/mariadb_testing_1000_1697773496.json b/bin/view/results/mariadb_testing_1000_1697773496.json new file mode 100644 index 000000000..6567e877a --- /dev/null +++ b/bin/view/results/mariadb_testing_1000_1697773496.json @@ -0,0 +1,47 @@ +[ + { + "roles": 2, + "results": { + "greaterThan, equal[1], limit(1000)": 0.017921924591064453, + "equal[3], limit(1000)": 0.018985986709594727, + "greaterThan, limit(1000)": 0.03374195098876953, + "contains[1], limit(1000)": 0.001703023910522461 + } + }, + { + "roles": 101, + "results": { + "greaterThan, equal[1], limit(1000)": 0.0020508766174316406, + "equal[3], limit(1000)": 0.012971878051757812, + "greaterThan, limit(1000)": 0.032111167907714844, + "contains[1], limit(1000)": 0.0015919208526611328 + } + }, + { + "roles": 490, + "results": { + "greaterThan, equal[1], limit(1000)": 0.0020859241485595703, + "equal[3], limit(1000)": 0.013467073440551758, + "greaterThan, limit(1000)": 0.032073974609375, + "contains[1], limit(1000)": 0.0016400814056396484 + } + }, + { + "roles": 946, + "results": { + "greaterThan, equal[1], limit(1000)": 0.002042055130004883, + "equal[3], limit(1000)": 0.013000011444091797, + "greaterThan, limit(1000)": 0.03235602378845215, + "contains[1], limit(1000)": 0.0015759468078613281 + } + }, + { + "roles": 1814, + "results": { + "greaterThan, equal[1], limit(1000)": 0.0020787715911865234, + "equal[3], limit(1000)": 0.01301884651184082, + "greaterThan, limit(1000)": 0.030966997146606445, + "contains[1], limit(1000)": 0.001605987548828125 + } + } +] \ No newline at end of file diff --git a/bin/view/results/mariadb_testing_1000_1697773900.json b/bin/view/results/mariadb_testing_1000_1697773900.json new file mode 100644 index 000000000..8ea5ed6af --- /dev/null +++ b/bin/view/results/mariadb_testing_1000_1697773900.json @@ -0,0 +1,52 @@ +[ + { + "roles": 2, + "results": { + "greaterThan, equal[1], limit(1000)": 0.0120849609375, + "equal[3], limit(1000)": 0.015228033065795898, + "greaterThan, limit(1000)": 0.03383207321166992, + "search, limit(1000)": 0.040261030197143555, + "contains[1], limit(1000)": 0.0017671585083007812 + } + }, + { + "roles": 101, + "results": { + "greaterThan, equal[1], limit(1000)": 0.0018150806427001953, + "equal[3], limit(1000)": 0.01302790641784668, + "greaterThan, limit(1000)": 0.03197622299194336, + "search, limit(1000)": 0.03850388526916504, + "contains[1], limit(1000)": 0.0015590190887451172 + } + }, + { + "roles": 491, + "results": { + "greaterThan, equal[1], limit(1000)": 0.0018579959869384766, + "equal[3], limit(1000)": 0.013176202774047852, + "greaterThan, limit(1000)": 0.03150510787963867, + "search, limit(1000)": 0.03767108917236328, + "contains[1], limit(1000)": 0.0016279220581054688 + } + }, + { + "roles": 952, + "results": { + "greaterThan, equal[1], limit(1000)": 0.001962900161743164, + "equal[3], limit(1000)": 0.013421058654785156, + "greaterThan, limit(1000)": 0.03141212463378906, + "search, limit(1000)": 0.03910017013549805, + "contains[1], limit(1000)": 0.0019600391387939453 + } + }, + { + "roles": 1825, + "results": { + "greaterThan, equal[1], limit(1000)": 0.0020799636840820312, + "equal[3], limit(1000)": 0.014293909072875977, + "greaterThan, limit(1000)": 0.0318300724029541, + "search, limit(1000)": 0.0378110408782959, + "contains[1], limit(1000)": 0.001756906509399414 + } + } +] \ No newline at end of file diff --git a/bin/view/results/mariadb_testing_1000_1697773977.json b/bin/view/results/mariadb_testing_1000_1697773977.json new file mode 100644 index 000000000..d19acc1a3 --- /dev/null +++ b/bin/view/results/mariadb_testing_1000_1697773977.json @@ -0,0 +1 @@ +[{"roles":2,"results":{"greaterThan, equal[1], limit(1000)":0.011281013488769531,"equal[3], limit(1000)":0.018298864364624023,"greaterThan, limit(1000)":0.03335213661193848,"search, limit(1000)":0.03667807579040527,"contains[1], limit(1000)":0.0016739368438720703}},{"roles":102,"results":{"greaterThan, equal[1], limit(1000)":0.001981973648071289,"equal[3], limit(1000)":0.012470006942749023,"greaterThan, limit(1000)":0.029846906661987305,"search, limit(1000)":0.036875009536743164,"contains[1], limit(1000)":0.001753091812133789}},{"roles":489,"results":{"greaterThan, equal[1], limit(1000)":0.0018579959869384766,"equal[3], limit(1000)":0.012539863586425781,"greaterThan, limit(1000)":0.03027510643005371,"search, limit(1000)":0.036364078521728516,"contains[1], limit(1000)":0.0017800331115722656}},{"roles":945,"results":{"greaterThan, equal[1], limit(1000)":0.0018270015716552734,"equal[3], limit(1000)":0.012778997421264648,"greaterThan, limit(1000)":0.0295259952545166,"search, limit(1000)":0.03641104698181152,"contains[1], limit(1000)":0.0015921592712402344}},{"roles":1811,"results":{"greaterThan, equal[1], limit(1000)":0.0018990039825439453,"equal[3], limit(1000)":0.012309074401855469,"greaterThan, limit(1000)":0.029526948928833008,"search, limit(1000)":0.03502988815307617,"contains[1], limit(1000)":0.0015959739685058594}}] \ No newline at end of file From f68bbef14036c4b90ba57c697706d5034439a89d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 20 Oct 2023 20:08:33 +1300 Subject: [PATCH 07/52] Add contains queries to benchmarks --- bin/cli.php | 3 +- bin/tasks/index.php | 20 +++++------ bin/tasks/load.php | 24 +++++++------ bin/tasks/query.php | 79 ++++++++++++++++++++--------------------- tests/Database/Base.php | 16 ++++----- 5 files changed, 73 insertions(+), 69 deletions(-) diff --git a/bin/cli.php b/bin/cli.php index bbb60df5a..d9932d0b2 100644 --- a/bin/cli.php +++ b/bin/cli.php @@ -1,10 +1,11 @@ task('index') ->desc('Index mock data for testing queries') - ->param('adapter', '', new Text(0), 'Database adapter', false) - ->param('name', '', new Text(0), 'Name of created database.', false) + ->param('adapter', '', new Text(0), 'Database adapter') + ->param('name', '', new Text(0), 'Name of created database.') ->action(function ($adapter, $name) { $namespace = '_ns'; $cache = new Cache(new NoCache()); @@ -74,36 +74,36 @@ $database->setDefaultDatabase($name); $database->setNamespace($namespace); - Console::info("For query: greaterThan(created, 2010-01-01 05:00:00)', 'equal(genre,travel)"); - + Console::info("greaterThan('created', ['2010-01-01 05:00:00']), equal('genre', ['travel'])"); $start = microtime(true); $database->createIndex('articles', 'createdGenre', Database::INDEX_KEY, ['created', 'genre'], [], [Database::ORDER_DESC, Database::ORDER_DESC]); $time = microtime(true) - $start; Console::success("{$time} seconds"); Console::info("equal('genre', ['fashion', 'finance', 'sports'])"); - $start = microtime(true); $database->createIndex('articles', 'genre', Database::INDEX_KEY, ['genre'], [], [Database::ORDER_ASC]); $time = microtime(true) - $start; Console::success("{$time} seconds"); - Console::info("greaterThan('views', 100000)"); - $start = microtime(true); $database->createIndex('articles', 'views', Database::INDEX_KEY, ['views'], [], [Database::ORDER_DESC]); $time = microtime(true) - $start; Console::success("{$time} seconds"); - Console::info("search('text', 'Alice')"); $start = microtime(true); $database->createIndex('articles', 'fulltextsearch', Database::INDEX_FULLTEXT, ['text']); $time = microtime(true) - $start; Console::success("{$time} seconds"); - }); + Console::info("contains('tags', ['tag1'])"); + $start = microtime(true); + $database->createIndex('articles', 'tags', Database::INDEX_KEY, ['tags']); + $time = microtime(true) - $start; + Console::success("{$time} seconds"); + }); $cli ->error() diff --git a/bin/tasks/load.php b/bin/tasks/load.php index 1a4804290..1827fdb58 100644 --- a/bin/tasks/load.php +++ b/bin/tasks/load.php @@ -27,7 +27,7 @@ /** * @Example - * docker-compose exec tests bin/load --adapter=mariadb --limit=1000 --name=testing + * docker compose exec tests bin/load --adapter=mariadb --limit=1000 --name=testing */ $cli @@ -44,6 +44,7 @@ Console::info("Filling {$adapter} with {$limit} records: {$name}"); Swoole\Runtime::enableCoroutine(); + switch ($adapter) { case 'mariadb': Co\run(function () use (&$start, $limit, $name, $namespace, $cache) { @@ -85,7 +86,7 @@ // A coroutine is assigned per 1000 documents for ($i=0; $i < $limit/1000; $i++) { - go(function () use ($pool, $faker, $name, $cache, $namespace) { + \go(function () use ($pool, $faker, $name, $cache, $namespace) { $pdo = $pool->get(); $database = new Database(new MariaDB($pdo), $cache); @@ -94,7 +95,7 @@ // Each coroutine loads 1000 documents for ($i=0; $i < 1000; $i++) { - addArticle($database, $faker); + createDocument($database, $faker); } // Reclaim resources @@ -146,7 +147,7 @@ // A coroutine is assigned per 1000 documents for ($i=0; $i < $limit/1000; $i++) { - go(function () use ($pool, $faker, $name, $cache, $namespace) { + \go(function () use ($pool, $faker, $name, $cache, $namespace) { $pdo = $pool->get(); $database = new Database(new MySQL($pdo), $cache); @@ -155,7 +156,7 @@ // Each coroutine loads 1000 documents for ($i=0; $i < 1000; $i++) { - addArticle($database, $faker); + createDocument($database, $faker); } // Reclaim resources @@ -197,7 +198,7 @@ // Each coroutine loads 1000 documents for ($i=0; $i < 1000; $i++) { - addArticle($database, $faker); + createDocument($database, $faker); } $database = null; @@ -233,25 +234,27 @@ function createSchema(Database $database): void $database->create(); Authorization::setRole(Role::any()->toString()); + + $database->createCollection('articles', permissions: [ Permission::create(Role::any()), Permission::read(Role::any()), ]); $database->createAttribute('articles', 'author', Database::VAR_STRING, 256, true); - $database->createAttribute('articles', 'created', Database::VAR_DATETIME, 0, true, null, false, false, null, [], ['datetime']); + $database->createAttribute('articles', 'created', Database::VAR_DATETIME, 0, true, filters: ['datetime']); $database->createAttribute('articles', 'text', Database::VAR_STRING, 5000, true); $database->createAttribute('articles', 'genre', Database::VAR_STRING, 256, true); $database->createAttribute('articles', 'views', Database::VAR_INTEGER, 0, true); + $database->createAttribute('articles', 'tags', Database::VAR_STRING, 0, true, array: true); $database->createIndex('articles', 'text', Database::INDEX_FULLTEXT, ['text']); } -function addArticle($database, Generator $faker): void +function createDocument($database, Generator $faker): void { $database->createDocument('articles', new Document([ // Five random users out of 10,000 get read access // Three random users out of 10,000 get mutate access - '$permissions' => [ Permission::read(Role::any()), Permission::read(Role::user($faker->randomNumber(9))), @@ -272,6 +275,7 @@ function addArticle($database, Generator $faker): void 'created' => \Utopia\Database\DateTime::format($faker->dateTime()), 'text' => $faker->realTextBetween(1000, 4000), 'genre' => $faker->randomElement(['fashion', 'food', 'travel', 'music', 'lifestyle', 'fitness', 'diy', 'sports', 'finance']), - 'views' => $faker->randomNumber(6) + 'views' => $faker->randomNumber(6), + 'tags' => $faker->randomElements(['short', 'quick', 'easy', 'medium', 'hard'], $faker->numberBetween(1, 5)), ])); } diff --git a/bin/tasks/query.php b/bin/tasks/query.php index ab7115563..e1ee5e27a 100644 --- a/bin/tasks/query.php +++ b/bin/tasks/query.php @@ -21,13 +21,13 @@ /** * @Example - * docker-compose exec tests bin/query --adapter=mariadb --limit=1000 --name=testing + * docker compose exec tests bin/query --adapter=mariadb --limit=1000 --name=testing */ $cli ->task('query') ->desc('Query mock data') - ->param('adapter', '', new Text(0), 'Database adapter', false) - ->param('name', '', new Text(0), 'Name of created database.', false) + ->param('adapter', '', new Text(0), 'Database adapter') + ->param('name', '', new Text(0), 'Name of created database.') ->param('limit', 25, new Numeric(), 'Limit on queried documents', true) ->action(function (string $adapter, string $name, int $limit) { $namespace = '_ns'; @@ -80,40 +80,39 @@ return; } - $faker = Factory::create(); $report = []; - $count = addRoles($faker, 1); + $count = setRoles($faker, 1); Console::info("\n{$count} roles:"); $report[] = [ 'roles' => $count, 'results' => runQueries($database, $limit) ]; - $count = addRoles($faker, 100); + $count = setRoles($faker, 100); Console::info("\n{$count} roles:"); $report[] = [ 'roles' => $count, 'results' => runQueries($database, $limit) ]; - $count = addRoles($faker, 400); + $count = setRoles($faker, 400); Console::info("\n{$count} roles:"); $report[] = [ 'roles' => $count, 'results' => runQueries($database, $limit) ]; - $count = addRoles($faker, 500); + $count = setRoles($faker, 500); Console::info("\n{$count} roles:"); $report[] = [ 'roles' => $count, 'results' => runQueries($database, $limit) ]; - $count = addRoles($faker, 1000); + $count = setRoles($faker, 1000); Console::info("\n{$count} roles:"); $report[] = [ 'roles' => $count, @@ -121,72 +120,72 @@ ]; if (!file_exists('bin/view/results')) { - mkdir('bin/view/results', 0777, true); + \mkdir('bin/view/results', 0777, true); } - $time = time(); - $f = fopen("bin/view/results/{$adapter}_{$name}_{$limit}_{$time}.json", 'w'); - fwrite($f, json_encode($report)); - fclose($f); + $time = \time(); + $results = \fopen("bin/view/results/{$adapter}_{$name}_{$limit}_{$time}.json", 'w'); + \fwrite($results, \json_encode($report)); + \fclose($results); }); - $cli -->error() -->inject('error') -->action(function (Exception $error) { - Console::error($error->getMessage()); -}); + ->error() + ->inject('error') + ->action(function (Exception $error) { + Console::error($error->getMessage()); + }); +function setRoles($faker, $count): int +{ + for ($i = 0; $i < $count; $i++) { + Authorization::setRole($faker->numerify('user####')); + } + return \count(Authorization::getRoles()); +} -function runQueries(Database $database, int $limit) +function runQueries(Database $database, int $limit): array { $results = []; - // Recent travel blogs - $results[] = runQuery([ + // Recent travel blogs + $results["Querying greater than, equal[1] and limit"] = runQuery([ Query::greaterThan('created', '2010-01-01 05:00:00'), Query::equal('genre', ['travel']), Query::limit($limit) ], $database); // Favorite genres - - $results[] = runQuery([ + $results["Querying equal[3] and limit"] = runQuery([ Query::equal('genre', ['fashion', 'finance', 'sports']), Query::limit($limit) ], $database); // Popular posts - - $results[] = runQuery([ + $results["Querying greaterThan, limit({$limit})"] = runQuery([ Query::greaterThan('views', 100000), Query::limit($limit) ], $database); // Fulltext search - - $results[] = runQuery([ + $results["Query search, limit({$limit})"] = runQuery([ Query::search('text', 'Alice'), Query::limit($limit) ], $database); - return $results; -} + // Tags contain query + $results["Querying contains[1], limit({$limit})"] = runQuery([ + Query::contains('tags', ['tag1']), + Query::limit($limit) + ], $database); -function addRoles($faker, $count) -{ - for ($i = 0; $i < $count; $i++) { - Authorization::setRole($faker->numerify('user####')); - } - return count(Authorization::getRoles()); + return $results; } function runQuery(array $query, Database $database) { - $info = array_map(function ($q) { - /** @var $q Query */ - return $q->getAttribute() . ' : ' . $q->getMethod() . ' : ' . implode(',', $q->getValues()); + $info = array_map(function (Query $q) { + return $q->getAttribute() . ': ' . $q->getMethod() . ' = ' . implode(',', $q->getValues()); }, $query); Console::log('Running query: [' . implode(', ', $info) . ']'); diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 4786d050a..bfb07fef4 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -1852,17 +1852,17 @@ public function testFindContains(): void $this->assertEquals(4, count($documents)); - $documents = static::getDatabase()->find('movies', [ - Query::contains('genres', ['non-existent']), - ]); + $documents = static::getDatabase()->find('movies', [ + Query::contains('genres', ['non-existent']), + ]); - $this->assertEquals(0, count($documents)); + $this->assertEquals(0, count($documents)); - $this->expectException(DatabaseException::class); + $this->expectException(DatabaseException::class); - static::getDatabase()->find('movies', [ - Query::contains('name', ['Frozen']), - ]); + static::getDatabase()->find('movies', [ + Query::contains('name', ['Frozen']), + ]); } public function testFindFulltext(): void From 893da6097ab943dfba0c39223803668916343397 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 25 Dec 2023 10:41:11 +0200 Subject: [PATCH 08/52] lock file --- composer.lock | 106 +++++++++++++++++++++++++------------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/composer.lock b/composer.lock index 3ab0753d4..db0f24431 100644 --- a/composer.lock +++ b/composer.lock @@ -268,16 +268,16 @@ }, { "name": "utopia-php/framework", - "version": "0.31.0", + "version": "0.31.1", "source": { "type": "git", "url": "https://github.com/utopia-php/framework.git", - "reference": "207f77378965fca9a9bc3783ea379d3549f86bc0" + "reference": "e50d2d16f4bc31319043f3f6d3dbea36c6fd6b68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/207f77378965fca9a9bc3783ea379d3549f86bc0", - "reference": "207f77378965fca9a9bc3783ea379d3549f86bc0", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/e50d2d16f4bc31319043f3f6d3dbea36c6fd6b68", + "reference": "e50d2d16f4bc31319043f3f6d3dbea36c6fd6b68", "shasum": "" }, "require": { @@ -307,9 +307,9 @@ ], "support": { "issues": "https://github.com/utopia-php/framework/issues", - "source": "https://github.com/utopia-php/framework/tree/0.31.0" + "source": "https://github.com/utopia-php/framework/tree/0.31.1" }, - "time": "2023-08-30T16:10:04+00:00" + "time": "2023-12-08T18:47:29+00:00" }, { "name": "utopia-php/mongo", @@ -638,16 +638,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.17.1", + "version": "v4.18.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", "shasum": "" }, "require": { @@ -688,9 +688,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" }, - "time": "2023-08-13T19:53:39+00:00" + "time": "2023-12-10T21:03:43+00:00" }, { "name": "pcov/clobber", @@ -839,16 +839,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.35", + "version": "1.10.50", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "e730e5facb75ffe09dfb229795e8c01a459f26c3" + "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e730e5facb75ffe09dfb229795e8c01a459f26c3", - "reference": "e730e5facb75ffe09dfb229795e8c01a459f26c3", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/06a98513ac72c03e8366b5a0cb00750b487032e4", + "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4", "shasum": "" }, "require": { @@ -897,27 +897,27 @@ "type": "tidelift" } ], - "time": "2023-09-19T15:27:56+00:00" + "time": "2023-12-13T10:59:42+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.29", + "version": "9.2.30", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -967,7 +967,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" }, "funding": [ { @@ -975,7 +975,7 @@ "type": "github" } ], - "time": "2023-09-19T04:57:46+00:00" + "time": "2023-12-22T06:47:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1220,16 +1220,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.13", + "version": "9.6.15", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be" + "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/05017b80304e0eb3f31d90194a563fd53a6021f1", + "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1", "shasum": "" }, "require": { @@ -1303,7 +1303,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.13" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.15" }, "funding": [ { @@ -1319,7 +1319,7 @@ "type": "tidelift" } ], - "time": "2023-09-19T05:39:22+00:00" + "time": "2023-12-01T16:55:19+00:00" }, { "name": "psr/container", @@ -1663,20 +1663,20 @@ }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -1708,7 +1708,7 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" }, "funding": [ { @@ -1716,7 +1716,7 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", @@ -1990,20 +1990,20 @@ }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -2035,7 +2035,7 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" }, "funding": [ { @@ -2043,7 +2043,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", @@ -2428,7 +2428,7 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", @@ -2475,7 +2475,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.3.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" }, "funding": [ { @@ -2495,16 +2495,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", "shasum": "" }, "require": { @@ -2533,7 +2533,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.2" }, "funding": [ { @@ -2541,7 +2541,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2023-11-20T00:12:19+00:00" }, { "name": "utopia-php/cli", @@ -2608,5 +2608,5 @@ "php": ">=8.0" }, "platform-dev": [], - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.6.0" } From 96c6945eb987852dc95b27d39c75d3f6e2b62c82 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 25 Dec 2023 11:10:25 +0200 Subject: [PATCH 09/52] Query name changes --- tests/Database/QueryTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index fc57bfea4..71557171b 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -24,9 +24,9 @@ public function testCreate(): void $this->assertEquals('title', $query->getAttribute()); $this->assertEquals('Iron Man', $query->getValues()[0]); - $query = new Query(Query::TYPE_ORDERDESC, 'score'); + $query = new Query(Query::TYPE_ORDER_DESC, 'score'); - $this->assertEquals(Query::TYPE_ORDERDESC, $query->getMethod()); + $this->assertEquals(Query::TYPE_ORDER_DESC, $query->getMethod()); $this->assertEquals('score', $query->getAttribute()); $this->assertEquals([], $query->getValues()); @@ -56,7 +56,7 @@ public function testCreate(): void $query = Query::orderAsc('score'); - $this->assertEquals(Query::TYPE_ORDERASC, $query->getMethod()); + $this->assertEquals(Query::TYPE_ORDER_ASC, $query->getMethod()); $this->assertEquals('score', $query->getAttribute()); $this->assertEquals([], $query->getValues()); @@ -69,7 +69,7 @@ public function testCreate(): void $cursor = new Document(); $query = Query::cursorAfter($cursor); - $this->assertEquals(Query::TYPE_CURSORAFTER, $query->getMethod()); + $this->assertEquals(Query::TYPE_CURSOR_AFTER, $query->getMethod()); $this->assertEquals('', $query->getAttribute()); $this->assertEquals([$cursor], $query->getValues()); @@ -446,12 +446,12 @@ public function testisMethod(): void $this->assertTrue(Query::isMethod(Query::TYPE_GREATER_EQUAL)); $this->assertTrue(Query::isMethod(Query::TYPE_CONTAINS)); $this->assertTrue(Query::isMethod(QUERY::TYPE_SEARCH)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_ORDERASC)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_ORDERDESC)); + $this->assertTrue(Query::isMethod(QUERY::TYPE_ORDER_ASC)); + $this->assertTrue(Query::isMethod(QUERY::TYPE_ORDER_DESC)); $this->assertTrue(Query::isMethod(QUERY::TYPE_LIMIT)); $this->assertTrue(Query::isMethod(QUERY::TYPE_OFFSET)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_CURSORAFTER)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_CURSORBEFORE)); + $this->assertTrue(Query::isMethod(QUERY::TYPE_CURSOR_AFTER)); + $this->assertTrue(Query::isMethod(QUERY::TYPE_CURSOR_BEFORE)); $this->assertTrue(Query::isMethod(QUERY::TYPE_IS_NULL)); $this->assertTrue(Query::isMethod(QUERY::TYPE_IS_NOT_NULL)); $this->assertTrue(Query::isMethod(QUERY::TYPE_BETWEEN)); From 32d49fa908cf787f1ce89800c94813e9e6c8fb21 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 25 Dec 2023 14:19:56 +0200 Subject: [PATCH 10/52] Remove old parse tests --- tests/Database/QueryTest.php | 472 ----------------------------------- 1 file changed, 472 deletions(-) delete mode 100644 tests/Database/QueryTest.php diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php deleted file mode 100644 index 71557171b..000000000 --- a/tests/Database/QueryTest.php +++ /dev/null @@ -1,472 +0,0 @@ -assertEquals(Query::TYPE_EQUAL, $query->getMethod()); - $this->assertEquals('title', $query->getAttribute()); - $this->assertEquals('Iron Man', $query->getValues()[0]); - - $query = new Query(Query::TYPE_ORDER_DESC, 'score'); - - $this->assertEquals(Query::TYPE_ORDER_DESC, $query->getMethod()); - $this->assertEquals('score', $query->getAttribute()); - $this->assertEquals([], $query->getValues()); - - $query = new Query(Query::TYPE_LIMIT, values: [10]); - - $this->assertEquals(Query::TYPE_LIMIT, $query->getMethod()); - $this->assertEquals('', $query->getAttribute()); - $this->assertEquals(10, $query->getValues()[0]); - - $query = Query::equal('title', ['Iron Man']); - - $this->assertEquals(Query::TYPE_EQUAL, $query->getMethod()); - $this->assertEquals('title', $query->getAttribute()); - $this->assertEquals('Iron Man', $query->getValues()[0]); - - $query = Query::greaterThan('score', 10); - - $this->assertEquals(Query::TYPE_GREATER, $query->getMethod()); - $this->assertEquals('score', $query->getAttribute()); - $this->assertEquals(10, $query->getValues()[0]); - - $query = Query::search('search', 'John Doe'); - - $this->assertEquals(Query::TYPE_SEARCH, $query->getMethod()); - $this->assertEquals('search', $query->getAttribute()); - $this->assertEquals('John Doe', $query->getValues()[0]); - - $query = Query::orderAsc('score'); - - $this->assertEquals(Query::TYPE_ORDER_ASC, $query->getMethod()); - $this->assertEquals('score', $query->getAttribute()); - $this->assertEquals([], $query->getValues()); - - $query = Query::limit(10); - - $this->assertEquals(Query::TYPE_LIMIT, $query->getMethod()); - $this->assertEquals('', $query->getAttribute()); - $this->assertEquals([10], $query->getValues()); - - $cursor = new Document(); - $query = Query::cursorAfter($cursor); - - $this->assertEquals(Query::TYPE_CURSOR_AFTER, $query->getMethod()); - $this->assertEquals('', $query->getAttribute()); - $this->assertEquals([$cursor], $query->getValues()); - - $query = Query::isNull('title'); - - $this->assertEquals(Query::TYPE_IS_NULL, $query->getMethod()); - $this->assertEquals('title', $query->getAttribute()); - $this->assertEquals([], $query->getValues()); - - $query = Query::isNotNull('title'); - - $this->assertEquals(Query::TYPE_IS_NOT_NULL, $query->getMethod()); - $this->assertEquals('title', $query->getAttribute()); - $this->assertEquals([], $query->getValues()); - } - - public function testParse(): void - { - $query = Query::parse('equal("title", "Iron Man")'); - - $this->assertEquals('equal', $query->getMethod()); - $this->assertEquals('title', $query->getAttribute()); - $this->assertEquals('Iron Man', $query->getValues()[0]); - - $query = Query::parse('lessThan("year", 2001)'); - - $this->assertEquals('lessThan', $query->getMethod()); - $this->assertEquals('year', $query->getAttribute()); - $this->assertEquals(2001, $query->getValues()[0]); - - $query = Query::parse('equal("published", true)'); - - $this->assertEquals('equal', $query->getMethod()); - $this->assertEquals('published', $query->getAttribute()); - $this->assertTrue($query->getValues()[0]); - - $query = Query::parse('equal("published", false)'); - - $this->assertEquals('equal', $query->getMethod()); - $this->assertEquals('published', $query->getAttribute()); - $this->assertFalse($query->getValues()[0]); - - $query = Query::parse('equal("actors", [ " Johnny Depp ", " Brad Pitt" , \'Al Pacino \' ])'); - - $this->assertEquals('equal', $query->getMethod()); - $this->assertEquals('actors', $query->getAttribute()); - $this->assertEquals(" Johnny Depp ", $query->getValues()[0]); - $this->assertEquals(" Brad Pitt", $query->getValues()[1]); - $this->assertEquals("Al Pacino ", $query->getValues()[2]); - - $query = Query::parse('equal("actors", ["Brad Pitt", "Johnny Depp"])'); - - $this->assertEquals('equal', $query->getMethod()); - $this->assertEquals('actors', $query->getAttribute()); - $this->assertEquals("Brad Pitt", $query->getValues()[0]); - $this->assertEquals("Johnny Depp", $query->getValues()[1]); - - $query = Query::parse('contains("writers","Tim O\'Reilly")'); - - $this->assertEquals('contains', $query->getMethod()); - $this->assertEquals('writers', $query->getAttribute()); - $this->assertEquals("Tim O'Reilly", $query->getValues()[0]); - - $query = Query::parse('greaterThan("score", 8.5)'); - - $this->assertEquals('greaterThan', $query->getMethod()); - $this->assertEquals('score', $query->getAttribute()); - $this->assertEquals(8.5, $query->getValues()[0]); - - $query = Query::parse('notEqual("director", "null")'); - - $this->assertEquals('notEqual', $query->getMethod()); - $this->assertEquals('director', $query->getAttribute()); - $this->assertEquals('null', $query->getValues()[0]); - - $query = Query::parse('notEqual("director", null)'); - - $this->assertEquals('notEqual', $query->getMethod()); - $this->assertEquals('director', $query->getAttribute()); - $this->assertEquals(null, $query->getValues()[0]); - - $query = Query::parse('isNull("director")'); - - $this->assertEquals('isNull', $query->getMethod()); - $this->assertEquals('director', $query->getAttribute()); - $this->assertEquals([], $query->getValues()); - - $query = Query::parse('isNotNull("director")'); - - $this->assertEquals('isNotNull', $query->getMethod()); - $this->assertEquals('director', $query->getAttribute()); - $this->assertEquals([], $query->getValues()); - - $query = Query::parse('startsWith("director", "Quentin")'); - - $this->assertEquals('startsWith', $query->getMethod()); - $this->assertEquals('director', $query->getAttribute()); - $this->assertEquals(['Quentin'], $query->getValues()); - - $query = Query::parse('endsWith("director", "Tarantino")'); - - $this->assertEquals('endsWith', $query->getMethod()); - $this->assertEquals('director', $query->getAttribute()); - $this->assertEquals(['Tarantino'], $query->getValues()); - - $query = Query::parse('select(["title", "director"])'); - - $this->assertEquals('select', $query->getMethod()); - $this->assertEquals('', $query->getAttribute()); - $this->assertEquals('title', $query->getValues()[0]); - $this->assertEquals('director', $query->getValues()[1]); - - $query = Query::parse('between("age", 15, 18)'); - - $this->assertEquals('between', $query->getMethod()); - $this->assertEquals('age', $query->getAttribute()); - $this->assertEquals(15, $query->getValues()[0]); - $this->assertEquals(18, $query->getValues()[1]); - - $query = Query::parse('between("lastUpdate", "DATE1", "DATE2")'); - - $this->assertEquals('between', $query->getMethod()); - $this->assertEquals('lastUpdate', $query->getAttribute()); - $this->assertEquals('DATE1', $query->getValues()[0]); - $this->assertEquals('DATE2', $query->getValues()[1]); - } - - public function testParseV2(): void - { - $query = Query::parse('equal("attr", 1)'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("attr", $query->getAttribute()); - $this->assertEquals([1], $query->getValues()); - - $query = Query::parse('equal("attr", [0])'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("attr", $query->getAttribute()); - $this->assertEquals([0], $query->getValues()); - - $query = Query::parse('equal("attr", 0,)'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("attr", $query->getAttribute()); - $this->assertEquals([0], $query->getValues()); - - $query = Query::parse('equal("attr", ["0"])'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("attr", $query->getAttribute()); - $this->assertEquals(["0"], $query->getValues()); - - $query = Query::parse('equal(1, ["[Hello] World"])'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals(1, $query->getAttribute()); - $this->assertEquals("[Hello] World", $query->getValues()[0]); - - $query = Query::parse('equal(1, , , ["[Hello] World"], , , )'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals(1, $query->getAttribute()); - $this->assertEquals("[Hello] World", $query->getValues()[0]); - - $query = Query::parse('equal(1, ["(Hello) World"])'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals(1, $query->getAttribute()); - $this->assertEquals("(Hello) World", $query->getValues()[0]); - - $query = Query::parse('equal(1, ["Hello , World"])'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals(1, $query->getAttribute()); - $this->assertEquals("Hello , World", $query->getValues()[0]); - - $query = Query::parse('equal(1, ["Hello , World"])'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals(1, $query->getAttribute()); - $this->assertEquals("Hello , World", $query->getValues()[0]); - - $query = Query::parse('equal(1, ["Hello /\ World"])'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals(1, $query->getAttribute()); - $this->assertEquals("Hello /\ World", $query->getValues()[0]); - - $query = Query::parse('equal(1, ["I\'m [**awesome**], \"Dev\"eloper"])'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals(1, $query->getAttribute()); - $this->assertEquals("I'm [**awesome**], \"Dev\"eloper", $query->getValues()[0]); - - $query = Query::parse('equal(1, "\\\\")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals(1, $query->getAttribute()); - $this->assertEquals("\\\\", $query->getValues()[0]); - - $query = Query::parse('equal(1, "Hello\\\\")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals(1, $query->getAttribute()); - $this->assertEquals("Hello\\\\", $query->getValues()[0]); - - $query = Query::parse('equal(1, "Hello\\\\", "World")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals(1, $query->getAttribute()); - $this->assertEquals("Hello\\\\", $query->getValues()[0]); - - $query = Query::parse('equal(1, "Hello\\", World")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals(1, $query->getAttribute()); - $this->assertEquals("Hello\", World", $query->getValues()[0]); - - $query = Query::parse('equal(1, "Hello\\\\\\", ", "World")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals(1, $query->getAttribute()); - $this->assertEquals("Hello\\\\\", ", $query->getValues()[0]); - - $query = Query::parse('equal()'); - $this->assertCount(0, $query->getValues()); - $this->assertEquals('', $query->getAttribute()); - $this->assertEquals(null, $query->getValue()); - - $query = Query::parse('limit()'); - $this->assertCount(0, $query->getValues()); - $this->assertEquals('', $query->getAttribute()); - $this->assertEquals(null, $query->getValue()); - - $query = Query::parse('offset()'); - $this->assertCount(0, $query->getValues()); - $this->assertEquals('', $query->getAttribute()); - $this->assertEquals(null, $query->getValue()); - - $query = Query::parse('cursorAfter()'); - $this->assertCount(0, $query->getValues()); - $this->assertEquals('', $query->getAttribute()); - $this->assertEquals(null, $query->getValue()); - - $query = Query::parse('orderDesc()'); - $this->assertCount(0, $query->getValues()); - $this->assertEquals('', $query->getAttribute()); - $this->assertEquals(null, $query->getValue()); - - $query = Query::parse('equal("count", 0)'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("count", $query->getAttribute()); - $this->assertEquals(0, $query->getValue()); - - $query = Query::parse('equal("value", "NormalString")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("value", $query->getAttribute()); - $this->assertEquals("NormalString", $query->getValue()); - - $query = Query::parse('equal("value", "{"type":"json","somekey":"someval"}")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("value", $query->getAttribute()); - $this->assertEquals('{"type":"json","somekey":"someval"}', $query->getValue()); - - $query = Query::parse('equal("value", "{ NormalStringInBraces }")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("value", $query->getAttribute()); - $this->assertEquals('{ NormalStringInBraces }', $query->getValue()); - - $query = Query::parse('equal("value", ""NormalStringInDoubleQuotes"")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("value", $query->getAttribute()); - $this->assertEquals('"NormalStringInDoubleQuotes"', $query->getValue()); - - $query = Query::parse('equal("value", "{"NormalStringInDoubleQuotesAndBraces"}")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("value", $query->getAttribute()); - $this->assertEquals('{"NormalStringInDoubleQuotesAndBraces"}', $query->getValue()); - - $query = Query::parse('equal("value", "\'NormalStringInSingleQuotes\'")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("value", $query->getAttribute()); - $this->assertEquals('\'NormalStringInSingleQuotes\'', $query->getValue()); - - $query = Query::parse('equal("value", "{\'NormalStringInSingleQuotesAndBraces\'}")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("value", $query->getAttribute()); - $this->assertEquals('{\'NormalStringInSingleQuotesAndBraces\'}', $query->getValue()); - - $query = Query::parse('equal("value", "SingleQuote\'InMiddle")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("value", $query->getAttribute()); - $this->assertEquals('SingleQuote\'InMiddle', $query->getValue()); - - $query = Query::parse('equal("value", "DoubleQuote"InMiddle")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("value", $query->getAttribute()); - $this->assertEquals('DoubleQuote"InMiddle', $query->getValue()); - - $query = Query::parse('equal("value", "Slash/InMiddle")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("value", $query->getAttribute()); - $this->assertEquals('Slash/InMiddle', $query->getValue()); - - $query = Query::parse('equal("value", "Backslash\InMiddle")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("value", $query->getAttribute()); - $this->assertEquals('Backslash\InMiddle', $query->getValue()); - - $query = Query::parse('equal("value", "Colon:InMiddle")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("value", $query->getAttribute()); - $this->assertEquals('Colon:InMiddle', $query->getValue()); - - $query = Query::parse('equal("value", ""quoted":"colon"")'); - $this->assertCount(1, $query->getValues()); - $this->assertEquals("value", $query->getAttribute()); - $this->assertEquals('"quoted":"colon"', $query->getValue()); - } - - public function testParseComplex(): void - { - $queries = [ - Query::parse('equal("One",[55.55,\'Works\',true])'), - // Same query with random spaces - Query::parse('equal("One" , [55.55, \'Works\',true])') - ]; - - foreach ($queries as $query) { - $this->assertEquals('equal', $query->getMethod()); - - $this->assertIsString($query->getAttribute()); - $this->assertEquals('One', $query->getAttribute()); - - $this->assertCount(3, $query->getValues()); - - $this->assertIsNumeric($query->getValues()[0]); - $this->assertEquals(55.55, $query->getValues()[0]); - - $this->assertEquals('Works', $query->getValues()[1]); - - $this->assertTrue($query->getValues()[2]); - } - } - - public function testGetAttribute(): void - { - $query = Query::parse('equal("title", "Iron Man")'); - - $this->assertIsArray($query->getValues()); - $this->assertCount(1, $query->getValues()); - $this->assertEquals('title', $query->getAttribute()); - $this->assertEquals('Iron Man', $query->getValues()[0]); - } - - public function testGetMethod(): void - { - $query = Query::parse('equal("title", "Iron Man")'); - - $this->assertEquals('equal', $query->getMethod()); - } - - public function testisMethod(): void - { - $this->assertTrue(Query::isMethod('equal')); - $this->assertTrue(Query::isMethod('notEqual')); - $this->assertTrue(Query::isMethod('lessThan')); - $this->assertTrue(Query::isMethod('lessThanEqual')); - $this->assertTrue(Query::isMethod('greaterThan')); - $this->assertTrue(Query::isMethod('greaterThanEqual')); - $this->assertTrue(Query::isMethod('contains')); - $this->assertTrue(Query::isMethod('search')); - $this->assertTrue(Query::isMethod('orderDesc')); - $this->assertTrue(Query::isMethod('orderAsc')); - $this->assertTrue(Query::isMethod('limit')); - $this->assertTrue(Query::isMethod('offset')); - $this->assertTrue(Query::isMethod('cursorAfter')); - $this->assertTrue(Query::isMethod('cursorBefore')); - $this->assertTrue(Query::isMethod('isNull')); - $this->assertTrue(Query::isMethod('isNotNull')); - $this->assertTrue(Query::isMethod('between')); - $this->assertTrue(Query::isMethod('select')); - - $this->assertTrue(Query::isMethod(Query::TYPE_EQUAL)); - $this->assertTrue(Query::isMethod(Query::TYPE_NOT_EQUAL)); - $this->assertTrue(Query::isMethod(Query::TYPE_LESSER)); - $this->assertTrue(Query::isMethod(Query::TYPE_LESSER_EQUAL)); - $this->assertTrue(Query::isMethod(Query::TYPE_GREATER)); - $this->assertTrue(Query::isMethod(Query::TYPE_GREATER_EQUAL)); - $this->assertTrue(Query::isMethod(Query::TYPE_CONTAINS)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_SEARCH)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_ORDER_ASC)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_ORDER_DESC)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_LIMIT)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_OFFSET)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_CURSOR_AFTER)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_CURSOR_BEFORE)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_IS_NULL)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_IS_NOT_NULL)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_BETWEEN)); - $this->assertTrue(Query::isMethod(QUERY::TYPE_SELECT)); - - /* - Tests for aliases if we enable them: - $this->assertTrue(Query::isMethod('lt')); - $this->assertTrue(Query::isMethod('lte')); - $this->assertTrue(Query::isMethod('gt')); - $this->assertTrue(Query::isMethod('gte')); - $this->assertTrue(Query::isMethod('eq')); - */ - - $this->assertFalse(Query::isMethod('invalid')); - $this->assertFalse(Query::isMethod('lte ')); - } -} From 4149b7a480da73eda9b9d86323013bad84cf9ad2 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 26 Dec 2023 11:05:43 +0200 Subject: [PATCH 11/52] array changes and new test --- bin/tasks/index.php | 10 +- bin/tasks/load.php | 10 +- phpunit.xml | 2 +- src/Database/Adapter/MariaDB.php | 28 +++--- src/Database/Adapter/Postgres.php | 28 +++--- src/Database/Adapter/SQLite.php | 8 +- src/Database/Database.php | 26 ++++-- src/Database/Validator/Query/Filter.php | 12 ++- tests/e2e/Adapter/Base.php | 108 +++++++++++++++++++++- tests/unit/Validator/Query/FilterTest.php | 9 +- 10 files changed, 179 insertions(+), 62 deletions(-) diff --git a/bin/tasks/index.php b/bin/tasks/index.php index ef1e45089..d84e36d93 100644 --- a/bin/tasks/index.php +++ b/bin/tasks/index.php @@ -98,11 +98,11 @@ $time = microtime(true) - $start; Console::success("{$time} seconds"); - Console::info("contains('tags', ['tag1'])"); - $start = microtime(true); - $database->createIndex('articles', 'tags', Database::INDEX_KEY, ['tags']); - $time = microtime(true) - $start; - Console::success("{$time} seconds"); + Console::info("contains('tags', ['tag1'])"); + $start = microtime(true); + $database->createIndex('articles', 'tags', Database::INDEX_KEY, ['tags']); + $time = microtime(true) - $start; + Console::success("{$time} seconds"); }); $cli diff --git a/bin/tasks/load.php b/bin/tasks/load.php index d5decdbd7..5c409da7a 100644 --- a/bin/tasks/load.php +++ b/bin/tasks/load.php @@ -85,7 +85,7 @@ ); // A coroutine is assigned per 1000 documents - for ($i=0; $i < $limit/1000; $i++) { + for ($i = 0; $i < $limit / 1000; $i++) { \go(function () use ($pool, $faker, $name, $cache, $namespace) { $pdo = $pool->get(); @@ -94,7 +94,7 @@ $database->setNamespace($namespace); // Each coroutine loads 1000 documents - for ($i=0; $i < 1000; $i++) { + for ($i = 0; $i < 1000; $i++) { createDocument($database, $faker); } @@ -146,7 +146,7 @@ ); // A coroutine is assigned per 1000 documents - for ($i=0; $i < $limit/1000; $i++) { + for ($i = 0; $i < $limit / 1000; $i++) { \go(function () use ($pool, $faker, $name, $cache, $namespace) { $pdo = $pool->get(); @@ -155,7 +155,7 @@ $database->setNamespace($namespace); // Each coroutine loads 1000 documents - for ($i=0; $i < 1000; $i++) { + for ($i = 0; $i < 1000; $i++) { createDocument($database, $faker); } @@ -197,7 +197,7 @@ $database->setNamespace($namespace); // Each coroutine loads 1000 documents - for ($i=0; $i < 1000; $i++) { + for ($i = 0; $i < 1000; $i++) { createDocument($database, $faker); } diff --git a/phpunit.xml b/phpunit.xml index ccdaa969e..783265d80 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false"> + stopOnFailure="true"> ./tests/unit diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 7f60a9cae..b71a64ac0 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -83,11 +83,13 @@ public function createCollection(string $name, array $attributes = [], array $in foreach ($attributes as $key => $attribute) { $attrId = $this->filter($attribute->getId()); - $attrType = $this->getSQLType($attribute->getAttribute('type'), $attribute->getAttribute('size', 0), $attribute->getAttribute('signed', true)); - if ($attribute->getAttribute('array')) { - $attrType = 'LONGTEXT'; - } + $attrType = $this->getSQLType( + $attribute->getAttribute('type'), + $attribute->getAttribute('size', 0), + $attribute->getAttribute('signed', true), + $attribute->getAttribute('array', false) + ); $attributeStrings[$key] = "`{$attrId}` {$attrType}, "; } @@ -268,7 +270,7 @@ public function createAttribute(string $collection, string $id, string $type, in { $name = $this->filter($collection); $id = $this->filter($id); - $type = $this->getSQLType($type, $size, $signed); + $type = $this->getSQLType($type, $size, $signed, $array); if ($array) { $type = 'JSON'; @@ -299,11 +301,7 @@ public function updateAttribute(string $collection, string $id, string $type, in { $name = $this->filter($collection); $id = $this->filter($id); - $type = $this->getSQLType($type, $size, $signed); - - if ($array) { - $type = 'JSON'; - } + $type = $this->getSQLType($type, $size, $signed, $array); $sql = "ALTER TABLE {$this->getSQLTable($name)} MODIFY `{$id}` {$type};"; @@ -1609,7 +1607,6 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, "; $sql = $this->trigger(Database::EVENT_DOCUMENT_FIND, $sql); - $stmt = $this->getPDO()->prepare($sql); foreach ($queries as $query) { @@ -1943,11 +1940,16 @@ protected function getSQLCondition(Query $query): string * @param string $type * @param int $size * @param bool $signed + * @param bool $array * @return string - * @throws Exception + * @throws DatabaseException */ - protected function getSQLType(string $type, int $size, bool $signed = true): string + protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false): string { + if($array === true){ + return 'JSON'; + } + switch ($type) { case Database::VAR_STRING: // $size = $size * 4; // Convert utf8mb4 size to bytes diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index f007f4b5a..18cc8acb7 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -86,11 +86,13 @@ public function createCollection(string $name, array $attributes = [], array $in foreach ($attributes as &$attribute) { $attrId = $this->filter($attribute->getId()); - $attrType = $this->getSQLType($attribute->getAttribute('type'), $attribute->getAttribute('size', 0), $attribute->getAttribute('signed', true)); - if ($attribute->getAttribute('array')) { - $attrType = 'TEXT'; - } + $attrType = $this->getSQLType( + $attribute->getAttribute('type'), + $attribute->getAttribute('size', 0), + $attribute->getAttribute('signed', true), + $attribute->getAttribute('array', false) + ); $attribute = "\"{$attrId}\" {$attrType}, "; } @@ -261,11 +263,7 @@ public function createAttribute(string $collection, string $id, string $type, in { $name = $this->filter($collection); $id = $this->filter($id); - $type = $this->getSQLType($type, $size, $signed); - - if ($array) { - $type = 'JSONB'; - } + $type = $this->getSQLType($type, $size, $signed, $array); $sql = " ALTER TABLE {$this->getSQLTable($name)} @@ -350,11 +348,7 @@ public function updateAttribute(string $collection, string $id, string $type, in { $name = $this->filter($collection); $id = $this->filter($id); - $type = $this->getSQLType($type, $size, $signed); - - if ($array) { - $type = 'JSONB'; - } + $type = $this->getSQLType($type, $size, $signed, $array); if ($type == 'TIMESTAMP(3)') { $type = "TIMESTAMP(3) without time zone USING TO_TIMESTAMP(\"$id\", 'YYYY-MM-DD HH24:MI:SS.MS')"; @@ -1937,8 +1931,12 @@ protected function getFulltextValue(string $value): string * * @return string */ - protected function getSQLType(string $type, int $size, bool $signed = true): string + protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false): string { + if($array === true){ + return 'JSONB'; + } + switch ($type) { case Database::VAR_STRING: // $size = $size * 4; // Convert utf8mb4 size to bytes diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 703f06690..b8b9b9085 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -106,7 +106,6 @@ public function delete(string $name): bool */ public function createCollection(string $name, array $attributes = [], array $indexes = []): bool { - $namespace = $this->getNamespace(); $id = $this->filter($name); try { @@ -124,13 +123,10 @@ public function createCollection(string $name, array $attributes = [], array $in $attrType = $this->getSQLType( $attribute->getAttribute('type'), $attribute->getAttribute('size', 0), - $attribute->getAttribute('signed', true) + $attribute->getAttribute('signed', true), + $attribute->getAttribute('array', false) ); - if ($attribute->getAttribute('array')) { - $attrType = 'LONGTEXT'; - } - $attributeStrings[$key] = "`{$attrId}` {$attrType}, "; } diff --git a/src/Database/Database.php b/src/Database/Database.php index bca9d818a..dfc660e1a 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1175,6 +1175,9 @@ public function createAttribute(string $collection, string $id, string $type, in if ($size > $this->adapter->getLimitForString()) { throw new DatabaseException('Max size allowed for string is: ' . number_format($this->adapter->getLimitForString())); } + else if ($size < 1) { + throw new DatabaseException('Min size is 1'); + } break; case self::VAR_INTEGER: @@ -4559,6 +4562,8 @@ public function find(string $collection, array $queries = []): array $cursor = empty($cursor) ? [] : $this->encode($collection, $cursor)->getArrayCopy(); + /** @var array $queries */ + $queries = \array_merge( $selects, self::convertQueries($collection, $filters) @@ -4569,16 +4574,17 @@ public function find(string $collection, array $queries = []): array foreach ($queries as $index => &$query) { switch ($query->getMethod()) { - case Query::TYPE_CONTAINS: - $attribute = $query->getAttribute(); - foreach ($collection->getAttribute('attributes', []) as $attr) { - $key = $attr->getAttribute('key', $attr->getAttribute('$id')); - $array = $attr->getAttribute('array', false); - if ($key === $attribute && !$array) { - throw new DatabaseException('Cannot query contains on attribute "' . $attribute . '" because it is not an array.'); - } - } - break; + // todo: moved to validator.... + // case Query::TYPE_CONTAINS: + // $attribute = $query->getAttribute(); + // foreach ($collection->getAttribute('attributes', []) as $attr) { + // $key = $attr->getAttribute('key', $attr->getAttribute('$id')); + // $array = $attr->getAttribute('array', false); + // if ($key === $attribute && !$array) { + // throw new DatabaseException('Cannot query contains on attribute "' . $attribute . '" because it is not an array.'); + // } + // } + // break; case Query::TYPE_SELECT: $values = $query->getValues(); foreach ($values as $valueIndex => $value) { diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index 7b576136c..f5b798de2 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -143,8 +143,18 @@ public function isValid($value): bool return false; } - return $this->isValidAttributeAndValues($attribute, $value->getValues()); + if(!$this->isValidAttributeAndValues($attribute, $value->getValues())) { + return false; + } + if(Query::TYPE_CONTAINS === $method) { + if($this->schema[$attribute]['array'] === false) { + $this->message = 'Cannot query contains on attribute "' . $attribute . '" because it is not an array.'; + return false; + } + } + + return true; case Query::TYPE_NOT_EQUAL: case Query::TYPE_LESSER: case Query::TYPE_LESSER_EQUAL: diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 195aefe0d..d31fb429d 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1591,6 +1591,100 @@ public function testDeleteDocument(Document $document): void } + /** + * @throws AuthorizationException + * @throws DuplicateException + * @throws ConflictException + * @throws LimitException + * @throws StructureException + * @throws DatabaseException + */ + public function testArrayAttribute(): array + { + Authorization::setRole(Role::any()->toString()); + + $collection = 'json'; + $permissions = [Permission::read(Role::any())]; + + static::getDatabase()->createCollection($collection, permissions: [ + Permission::create(Role::any()), + ]); + + $this->assertEquals(true, static::getDatabase()->createAttribute( + $collection, + 'age', + Database::VAR_INTEGER, + size: 0, + required: true, + )); + + $this->assertEquals(true, static::getDatabase()->createAttribute( + $collection, + 'active', + Database::VAR_BOOLEAN, + size: 0, + required: true, + array: true + )); + + $this->assertEquals(true, static::getDatabase()->createAttribute( + $collection, + 'names', + Database::VAR_STRING, + size: 50, + required: false, + array: true + )); + + $this->assertEquals(true, static::getDatabase()->createAttribute( + $collection, + 'numbers', + Database::VAR_INTEGER, + size: 0, + required: false, + signed: false, + array: true + )); + + try { + static::getDatabase()->createDocument($collection, new Document([])); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + $this->assertEquals('Invalid document structure: Missing required attribute "active"', $e->getMessage()); + } + + try { + static::getDatabase()->createDocument($collection, new Document([ + 'active' => [false], + 'names' => ['Joe', 100], + ])); + $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 50 chars', $e->getMessage()); + } + + try { + static::getDatabase()->createDocument($collection, new Document([ + 'active' => [false], + 'numbers' => [-100], + 'age' => [-20], + ])); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + $this->assertEquals('Should fails since it is Signed is false!', $e->getMessage()); + } + + $document = static::getDatabase()->createDocument($collection, new Document([ + '$permissions' => $permissions, + 'names' => ['Joe Baden', 'Antony Blinken', '100'], + 'numbers' => [0, -100, 999, 10.4], + ])); + + var_dump($document); + $this->assertEquals(0,1); + + } + /** * @return array */ @@ -1971,11 +2065,15 @@ public function testFindContains(): void $this->assertEquals(0, count($documents)); - $this->expectException(DatabaseException::class); - - static::getDatabase()->find('movies', [ - Query::contains('name', ['Frozen']), - ]); + try { + static::getDatabase()->find('movies', [ + Query::contains('name', ['Frozen']), + ]); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + $this->assertEquals('Invalid query: Cannot query contains on attribute "name" because it is not an array.', $e->getMessage()); + $this->assertTrue($e instanceof DatabaseException); + } } public function testFindFulltext(): void diff --git a/tests/unit/Validator/Query/FilterTest.php b/tests/unit/Validator/Query/FilterTest.php index 712dd9f49..c375033fd 100644 --- a/tests/unit/Validator/Query/FilterTest.php +++ b/tests/unit/Validator/Query/FilterTest.php @@ -23,6 +23,12 @@ public function setUp(): void 'type' => Database::VAR_STRING, 'array' => false, ]), + new Document([ + '$id' => 'attr_array', + 'key' => 'attr_array', + 'type' => Database::VAR_STRING, + 'array' => true, + ]), ], ); } @@ -34,7 +40,7 @@ public function testSuccess(): void $this->assertTrue($this->validator->isValid(Query::isNull('attr'))); $this->assertTrue($this->validator->isValid(Query::startsWith('attr', 'super'))); $this->assertTrue($this->validator->isValid(Query::endsWith('attr', 'man'))); - $this->assertTrue($this->validator->isValid(Query::contains('attr', ['super']))); + $this->assertTrue($this->validator->isValid(Query::contains('attr_array', ['super']))); } public function testFailure(): void @@ -57,6 +63,7 @@ public function testFailure(): void $this->assertFalse($this->validator->isValid(Query::orderDesc('attr'))); $this->assertFalse($this->validator->isValid(new Query(Query::TYPE_CURSOR_AFTER, values: ['asdf']))); $this->assertFalse($this->validator->isValid(new Query(Query::TYPE_CURSOR_BEFORE, values: ['asdf']))); + $this->assertFalse($this->validator->isValid(Query::contains('attr', ['super']))); } public function testEmptyValues(): void From 26a16f6d24631caa417018f190bdceedd0f139a6 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 26 Dec 2023 17:11:06 +0200 Subject: [PATCH 12/52] getSupportForIndexArray --- src/Database/Adapter.php | 7 ++++ src/Database/Adapter/MariaDB.php | 1 + src/Database/Adapter/Mongo.php | 10 ++++++ src/Database/Adapter/SQL.php | 10 ++++++ src/Database/Database.php | 5 +++ tests/e2e/Adapter/Base.php | 58 +++++++++++++++++++++++--------- 6 files changed, 75 insertions(+), 16 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 764f41df6..d50d7d8df 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -606,6 +606,13 @@ abstract public function getSupportForSchemas(): bool; */ abstract public function getSupportForIndex(): bool; + /** + * Is index array supported? + * + * @return bool + */ + abstract public function getSupportForIndexArray(): bool; + /** * Is unique index supported? * diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index b71a64ac0..720f04641 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1606,6 +1606,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, {$sqlLimit}; "; + var_dump($sql); $sql = $this->trigger(Database::EVENT_DOCUMENT_FIND, $sql); $stmt = $this->getPDO()->prepare($sql); diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index bf0971040..9be2b0429 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1557,6 +1557,16 @@ public function getSupportForIndex(): bool return true; } + /** + * Is index array supported? + * + * @return bool + */ + public function getSupportForIndexArray(): bool + { + return true; + } + /** * Is unique index supported? * diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 755d5e2d5..f3ed751c2 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -229,6 +229,16 @@ public function getSupportForIndex(): bool return true; } + /** + * Is index supported? + * + * @return bool + */ + public function getSupportForIndexArray(): bool + { + return true; + } + /** * Is unique index supported? * diff --git a/src/Database/Database.php b/src/Database/Database.php index dfc660e1a..b276eded5 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2366,6 +2366,11 @@ public function createIndex(string $collection, string $id, string $type, array } break; + case self::INDEX_ARRAY: + if (!$this->adapter->getSupportForIndexArray()) { + throw new DatabaseException('Key index array is not supported'); + } + break; default: throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT); } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index d31fb429d..de7b48726 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1599,7 +1599,7 @@ public function testDeleteDocument(Document $document): void * @throws StructureException * @throws DatabaseException */ - public function testArrayAttribute(): array + public function testArrayAttribute(): void { Authorization::setRole(Role::any()->toString()); @@ -1610,14 +1610,6 @@ public function testArrayAttribute(): array Permission::create(Role::any()), ]); - $this->assertEquals(true, static::getDatabase()->createAttribute( - $collection, - 'age', - Database::VAR_INTEGER, - size: 0, - required: true, - )); - $this->assertEquals(true, static::getDatabase()->createAttribute( $collection, 'active', @@ -1646,6 +1638,15 @@ public function testArrayAttribute(): array array: true )); + $this->assertEquals(true, static::getDatabase()->createAttribute( + $collection, + 'age', + Database::VAR_INTEGER, + size: 0, + required: false, + signed: false + )); + try { static::getDatabase()->createDocument($collection, new Document([])); $this->fail('Failed to throw exception'); @@ -1666,23 +1667,45 @@ public function testArrayAttribute(): array try { static::getDatabase()->createDocument($collection, new Document([ 'active' => [false], - 'numbers' => [-100], - 'age' => [-20], + 'age' => 1.5, ])); $this->fail('Failed to throw exception'); } catch(Throwable $e) { - $this->assertEquals('Should fails since it is Signed is false!', $e->getMessage()); + $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid integer', $e->getMessage()); + } + + try { + static::getDatabase()->createDocument($collection, new Document([ + 'active' => [false], + 'age' => -1, + ])); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + //$this->assertEquals('Should fail since it is Signed = false!!!!', $e->getMessage()); } $document = static::getDatabase()->createDocument($collection, new Document([ - '$permissions' => $permissions, - 'names' => ['Joe Baden', 'Antony Blinken', '100'], - 'numbers' => [0, -100, 999, 10.4], + 'active' => [false], + 'names' => ['Joe', 'Antony', '100'], + 'numbers' => [0, 100, 1000, -1], ])); + $this->assertEquals(false, $document->getAttribute('active')[0]); + $this->assertEquals('Antony', $document->getAttribute('names')[1]); + $this->assertEquals(100, $document->getAttribute('numbers')[1]); + +// try { +// todo: force create only INDEX_ARRAY for array???? +// static::getDatabase()->createIndex($collection, 'ind-names', Database::INDEX_FULLTEXT, ['names']); +// $this->fail('Failed to throw exception'); +// } catch(Throwable $e) { +// $this->assertEquals('Should this fail? can we create a fulltext index on array as users do today?', $e->getMessage()); +// } + + $this->assertTrue(true, static::getDatabase()->createIndex($collection, 'ind-names', Database::INDEX_ARRAY, ['names'])); + var_dump($document); $this->assertEquals(0,1); - } /** @@ -2074,6 +2097,9 @@ public function testFindContains(): void $this->assertEquals('Invalid query: Cannot query contains on attribute "name" because it is not an array.', $e->getMessage()); $this->assertTrue($e instanceof DatabaseException); } + + $this->assertTrue(false); + } public function testFindFulltext(): void From 2ea81a127956d7c7bc122a013ffcb03994e56a25 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 26 Dec 2023 17:51:32 +0200 Subject: [PATCH 13/52] find test --- src/Database/Adapter/MariaDB.php | 8 ++ src/Database/Adapter/MySQL.php | 2 +- src/Database/Adapter/SQL.php | 1 - tests/e2e/Adapter/Base.php | 16 ++- tests/e2e/Adapter/MariaDBTest.php | 118 +++++++++--------- tests/e2e/Adapter/MongoDBTest.php | 192 ++++++++++++++--------------- tests/e2e/Adapter/PostgresTest.php | 116 ++++++++--------- tests/e2e/Adapter/SQLiteTest.php | 128 +++++++++---------- 8 files changed, 299 insertions(+), 282 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 720f04641..d31b0b0db 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1926,6 +1926,14 @@ protected function getSQLCondition(Query $query): string } $condition = implode(' OR ', $conditions); return empty($condition) ? '' : '(' . $condition . ')'; + + +// $conditions = []; +// foreach ($query->getValues() as $key => $value) { +// $conditions[] = "JSON_CONTAINS(`table_main`.{$attribute}, :{$placeholder}_{$key})"; +// } +// $condition = implode(' OR ', $conditions); +// return empty($condition) ? '' : '(' . $condition . ')'; default: $conditions = []; foreach ($query->getValues() as $key => $value) { diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 23d98cadc..e7ee26857 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -31,7 +31,7 @@ protected function getSQLIndex(string $collection, string $id, string $type, arr $type = 'INDEX'; foreach ($attributes as $key => $value) { - $attributes[$key] = '(CAST(' . $value . ' AS char(255) ARRAY))'; + $attributes[$key] = '(CAST(`' . $value . '` AS char(255) ARRAY))'; } break; diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index f3ed751c2..007337ac2 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -955,7 +955,6 @@ public function getSQLConditions(array $queries = [], string $separator = 'AND') continue; } - /* @var $query Query */ if($query->isNested()) { $conditions[] = $this->getSQLConditions($query->getValues(), $query->getMethod()); } else { diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index de7b48726..c17dd4ef7 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1685,6 +1685,7 @@ public function testArrayAttribute(): void } $document = static::getDatabase()->createDocument($collection, new Document([ + '$permissions' => $permissions, 'active' => [false], 'names' => ['Joe', 'Antony', '100'], 'numbers' => [0, 100, 1000, -1], @@ -1702,10 +1703,19 @@ public function testArrayAttribute(): void // $this->assertEquals('Should this fail? can we create a fulltext index on array as users do today?', $e->getMessage()); // } - $this->assertTrue(true, static::getDatabase()->createIndex($collection, 'ind-names', Database::INDEX_ARRAY, ['names'])); + $this->assertTrue(true, static::getDatabase()->createIndex($collection, 'index-names', Database::INDEX_ARRAY, ['names'])); + + if ($this->getDatabase()->getAdapter()->getSupportForQueryContains()) { + $documents = static::getDatabase()->find($collection, [ + Query::contains('names', ['Jake', 'Joe']) + ]); + + $this->assertCount(1, $documents); + + var_dump($documents); + $this->assertEquals(true,false); + } - var_dump($document); - $this->assertEquals(0,1); } /** diff --git a/tests/e2e/Adapter/MariaDBTest.php b/tests/e2e/Adapter/MariaDBTest.php index e26ca69c2..fc3b8ce77 100644 --- a/tests/e2e/Adapter/MariaDBTest.php +++ b/tests/e2e/Adapter/MariaDBTest.php @@ -1,60 +1,60 @@ connect('redis', 6379); - $redis->flushAll(); - $cache = new Cache(new RedisAdapter($redis)); - - $database = new Database(new MariaDB($pdo), $cache); - $database->setDatabase('utopiaTests'); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); - - if ($database->exists('utopiaTests')) { - $database->delete('utopiaTests'); - } - - $database->create(); - - return self::$database = $database; - } -} +// +//namespace Tests\E2E\Adapter; +// +//use PDO; +//use Redis; +//use Utopia\Cache\Adapter\Redis as RedisAdapter; +//use Utopia\Cache\Cache; +//use Utopia\Database\Adapter\MariaDB; +//use Utopia\Database\Database; +// +//class MariaDBTest extends Base +//{ +// protected static ?Database $database = null; +// protected static string $namespace; +// +// // Remove once all methods are implemented +// /** +// * Return name of adapter +// * +// * @return string +// */ +// public static function getAdapterName(): string +// { +// return "mariadb"; +// } +// +// /** +// * @return Database +// */ +// public static function getDatabase(bool $fresh = false): Database +// { +// if (!is_null(self::$database) && !$fresh) { +// return self::$database; +// } +// +// $dbHost = 'mariadb'; +// $dbPort = '3306'; +// $dbUser = 'root'; +// $dbPass = 'password'; +// +// $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MariaDB::getPDOAttributes()); +// $redis = new Redis(); +// $redis->connect('redis', 6379); +// $redis->flushAll(); +// $cache = new Cache(new RedisAdapter($redis)); +// +// $database = new Database(new MariaDB($pdo), $cache); +// $database->setDatabase('utopiaTests'); +// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); +// +// if ($database->exists('utopiaTests')) { +// $database->delete('utopiaTests'); +// } +// +// $database->create(); +// +// return self::$database = $database; +// } +//} diff --git a/tests/e2e/Adapter/MongoDBTest.php b/tests/e2e/Adapter/MongoDBTest.php index 887c26ec2..c348bd331 100644 --- a/tests/e2e/Adapter/MongoDBTest.php +++ b/tests/e2e/Adapter/MongoDBTest.php @@ -1,97 +1,97 @@ connect('redis', 6379); - $redis->flushAll(); - $cache = new Cache(new RedisAdapter($redis)); - - $schema = 'utopiaTests'; // same as $this->testDatabase - $client = new Client( - $schema, - 'mongo', - 27017, - 'root', - 'example', - false - ); - - $database = new Database(new Mongo($client), $cache); - $database->setDatabase($schema); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); - - if ($database->exists('utopiaTests')) { - $database->delete('utopiaTests'); - } - - $database->create(); - - return self::$database = $database; - } - - /** - * @throws Exception - */ - public function testCreateExistsDelete(): void - { - // Mongo creates databases on the fly, so exists would always pass. So we override this test to remove the exists check. - $this->assertNotNull(static::getDatabase()->create()); - $this->assertEquals(true, static::getDatabase()->delete($this->testDatabase)); - $this->assertEquals(true, static::getDatabase()->create()); - $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase)); - } - - public function testRenameAttribute(): void - { - $this->assertTrue(true); - } - - public function testRenameAttributeExisting(): void - { - $this->assertTrue(true); - } - - public function testUpdateAttributeStructure(): void - { - $this->assertTrue(true); - } - - public function testKeywords(): void - { - $this->assertTrue(true); - } -} +// +//namespace Tests\E2E\Adapter; +// +//use Exception; +//use Redis; +//use Utopia\Cache\Adapter\Redis as RedisAdapter; +//use Utopia\Cache\Cache; +//use Utopia\Database\Adapter\Mongo; +//use Utopia\Database\Database; +//use Utopia\Mongo\Client; +// +//class MongoDBTest extends Base +//{ +// public static ?Database $database = null; +// protected static string $namespace; +// +// /** +// * Return name of adapter +// * +// * @return string +// */ +// public static function getAdapterName(): string +// { +// return "mongodb"; +// } +// +// /** +// * @return Database +// * @throws Exception +// */ +// public static function getDatabase(): Database +// { +// if (!is_null(self::$database)) { +// return self::$database; +// } +// +// $redis = new Redis(); +// $redis->connect('redis', 6379); +// $redis->flushAll(); +// $cache = new Cache(new RedisAdapter($redis)); +// +// $schema = 'utopiaTests'; // same as $this->testDatabase +// $client = new Client( +// $schema, +// 'mongo', +// 27017, +// 'root', +// 'example', +// false +// ); +// +// $database = new Database(new Mongo($client), $cache); +// $database->setDatabase($schema); +// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); +// +// if ($database->exists('utopiaTests')) { +// $database->delete('utopiaTests'); +// } +// +// $database->create(); +// +// return self::$database = $database; +// } +// +// /** +// * @throws Exception +// */ +// public function testCreateExistsDelete(): void +// { +// // Mongo creates databases on the fly, so exists would always pass. So we override this test to remove the exists check. +// $this->assertNotNull(static::getDatabase()->create()); +// $this->assertEquals(true, static::getDatabase()->delete($this->testDatabase)); +// $this->assertEquals(true, static::getDatabase()->create()); +// $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase)); +// } +// +// public function testRenameAttribute(): void +// { +// $this->assertTrue(true); +// } +// +// public function testRenameAttributeExisting(): void +// { +// $this->assertTrue(true); +// } +// +// public function testUpdateAttributeStructure(): void +// { +// $this->assertTrue(true); +// } +// +// public function testKeywords(): void +// { +// $this->assertTrue(true); +// } +//} diff --git a/tests/e2e/Adapter/PostgresTest.php b/tests/e2e/Adapter/PostgresTest.php index 6a12ecd8c..048801724 100644 --- a/tests/e2e/Adapter/PostgresTest.php +++ b/tests/e2e/Adapter/PostgresTest.php @@ -1,59 +1,59 @@ connect('redis', 6379); - $redis->flushAll(); - $cache = new Cache(new RedisAdapter($redis)); - - $database = new Database(new Postgres($pdo), $cache); - $database->setDatabase('utopiaTests'); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); - - if ($database->exists('utopiaTests')) { - $database->delete('utopiaTests'); - } - - $database->create(); - - return self::$database = $database; - } -} +// +//namespace Tests\E2E\Adapter; +// +//use PDO; +//use Redis; +//use Utopia\Cache\Adapter\Redis as RedisAdapter; +//use Utopia\Cache\Cache; +//use Utopia\Database\Adapter\Postgres; +//use Utopia\Database\Database; +// +//class PostgresTest extends Base +//{ +// public static ?Database $database = null; +// protected static string $namespace; +// +// /** +// * Return name of adapter +// * +// * @return string +// */ +// public static function getAdapterName(): string +// { +// return "postgres"; +// } +// +// /** +// * @reture Adapter +// */ +// public static function getDatabase(): Database +// { +// if (!is_null(self::$database)) { +// return self::$database; +// } +// +// $dbHost = 'postgres'; +// $dbPort = '5432'; +// $dbUser = 'root'; +// $dbPass = 'password'; +// +// $pdo = new PDO("pgsql:host={$dbHost};port={$dbPort};", $dbUser, $dbPass, Postgres::getPDOAttributes()); +// $redis = new Redis(); +// $redis->connect('redis', 6379); +// $redis->flushAll(); +// $cache = new Cache(new RedisAdapter($redis)); +// +// $database = new Database(new Postgres($pdo), $cache); +// $database->setDatabase('utopiaTests'); +// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); +// +// if ($database->exists('utopiaTests')) { +// $database->delete('utopiaTests'); +// } +// +// $database->create(); +// +// return self::$database = $database; +// } +//} diff --git a/tests/e2e/Adapter/SQLiteTest.php b/tests/e2e/Adapter/SQLiteTest.php index 16c36f6fd..8451dfa91 100644 --- a/tests/e2e/Adapter/SQLiteTest.php +++ b/tests/e2e/Adapter/SQLiteTest.php @@ -1,65 +1,65 @@ connect('redis'); - $redis->flushAll(); - - $cache = new Cache(new RedisAdapter($redis)); - - $database = new Database(new SQLite($pdo), $cache); - $database->setDatabase('utopiaTests'); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); - - if ($database->exists()) { - $database->delete(); - } - - $database->create(); - - return self::$database = $database; - } -} +// +//namespace Tests\E2E\Adapter; +// +//use PDO; +//use Redis; +//use Utopia\Cache\Adapter\Redis as RedisAdapter; +//use Utopia\Cache\Cache; +//use Utopia\Database\Adapter\SQLite; +//use Utopia\Database\Database; +// +//class SQLiteTest extends Base +//{ +// public static ?Database $database = null; +// protected static string $namespace; +// +// // Remove once all methods are implemented +// /** +// * Return name of adapter +// * +// * @return string +// */ +// public static function getAdapterName(): string +// { +// return "sqlite"; +// } +// +// /** +// * @return Database +// */ +// public static function getDatabase(): Database +// { +// if (!is_null(self::$database)) { +// return self::$database; +// } +// +// $db = __DIR__."/database.sql"; +// +// if (file_exists($db)) { +// unlink($db); +// } +// +// $dsn = $db; +// //$dsn = 'memory'; // Overwrite for fast tests +// $pdo = new PDO("sqlite:" . $dsn, null, null, SQLite::getPDOAttributes()); +// +// $redis = new Redis(); +// $redis->connect('redis'); +// $redis->flushAll(); +// +// $cache = new Cache(new RedisAdapter($redis)); +// +// $database = new Database(new SQLite($pdo), $cache); +// $database->setDatabase('utopiaTests'); +// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); +// +// if ($database->exists()) { +// $database->delete(); +// } +// +// $database->create(); +// +// return self::$database = $database; +// } +//} From 54f616625ea893c14eafcd5aa68ee54da51c01b8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 27 Dec 2023 18:52:54 +0200 Subject: [PATCH 14/52] tests --- composer.lock | 12 ++++++------ src/Database/Adapter/MariaDB.php | 2 +- src/Database/Adapter/MySQL.php | 2 +- src/Database/Database.php | 3 --- tests/e2e/Adapter/Base.php | 8 +++++--- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/composer.lock b/composer.lock index 7afcb5ffe..1a6b42b8a 100644 --- a/composer.lock +++ b/composer.lock @@ -268,16 +268,16 @@ }, { "name": "utopia-php/framework", - "version": "0.31.1", + "version": "0.32.0", "source": { "type": "git", "url": "https://github.com/utopia-php/framework.git", - "reference": "e50d2d16f4bc31319043f3f6d3dbea36c6fd6b68" + "reference": "ad6f7e6d6b38cf5bed4e3af9a1394c59d4bb9225" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/e50d2d16f4bc31319043f3f6d3dbea36c6fd6b68", - "reference": "e50d2d16f4bc31319043f3f6d3dbea36c6fd6b68", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/ad6f7e6d6b38cf5bed4e3af9a1394c59d4bb9225", + "reference": "ad6f7e6d6b38cf5bed4e3af9a1394c59d4bb9225", "shasum": "" }, "require": { @@ -307,9 +307,9 @@ ], "support": { "issues": "https://github.com/utopia-php/framework/issues", - "source": "https://github.com/utopia-php/framework/tree/0.31.1" + "source": "https://github.com/utopia-php/framework/tree/0.32.0" }, - "time": "2023-12-08T18:47:29+00:00" + "time": "2023-12-26T14:18:36+00:00" }, { "name": "utopia-php/mongo", diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index d31b0b0db..ce341f601 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -641,7 +641,7 @@ public function createIndex(string $collection, string $id, string $type, array } $sql = $this->getSQLIndex($name, $id, $type, $attributes); - + var_dump($sql); $sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql); return $this->getPDO() diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index e7ee26857..23d98cadc 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -31,7 +31,7 @@ protected function getSQLIndex(string $collection, string $id, string $type, arr $type = 'INDEX'; foreach ($attributes as $key => $value) { - $attributes[$key] = '(CAST(`' . $value . '` AS char(255) ARRAY))'; + $attributes[$key] = '(CAST(' . $value . ' AS char(255) ARRAY))'; } break; diff --git a/src/Database/Database.php b/src/Database/Database.php index b276eded5..3f8f133bb 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1175,9 +1175,6 @@ public function createAttribute(string $collection, string $id, string $type, in if ($size > $this->adapter->getLimitForString()) { throw new DatabaseException('Max size allowed for string is: ' . number_format($this->adapter->getLimitForString())); } - else if ($size < 1) { - throw new DatabaseException('Min size is 1'); - } break; case self::VAR_INTEGER: diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index c17dd4ef7..a413b7774 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1623,7 +1623,7 @@ public function testArrayAttribute(): void $collection, 'names', Database::VAR_STRING, - size: 50, + size: 255, required: false, array: true )); @@ -1661,7 +1661,7 @@ 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 50 chars', $e->getMessage()); + $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 { @@ -1703,7 +1703,9 @@ public function testArrayAttribute(): void // $this->assertEquals('Should this fail? can we create a fulltext index on array as users do today?', $e->getMessage()); // } - $this->assertTrue(true, static::getDatabase()->createIndex($collection, 'index-names', Database::INDEX_ARRAY, ['names'])); + $this->assertTrue(true, static::getDatabase()->createIndex($collection, 'indx-numbers', Database::INDEX_ARRAY, ['numbers'], [], [])); + + //$this->assertTrue(true, static::getDatabase()->createIndex($collection, 'indx-names', Database::INDEX_ARRAY, ['age', 'names'], [], ['asc', 'desc'])); if ($this->getDatabase()->getAdapter()->getSupportForQueryContains()) { $documents = static::getDatabase()->find($collection, [ From 361f379710ec00a74d5491dd085d9fdc115c4cc8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 28 Dec 2023 12:25:00 +0200 Subject: [PATCH 15/52] try json_overlaps --- src/Database/Adapter/MariaDB.php | 9 +-- tests/e2e/Adapter/Base.php | 12 +++ tests/e2e/Adapter/MariaDBTest.php | 118 ++++++++++++++--------------- tests/e2e/Adapter/MySQLTest.php | 122 +++++++++++++++--------------- 4 files changed, 133 insertions(+), 128 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index ce341f601..6aed86337 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1920,20 +1920,13 @@ protected function getSQLCondition(Query $query): string return "`table_main`.{$attribute} {$this->getSQLOperator($query->getMethod())}"; case Query::TYPE_CONTAINS: + // todo: change to JSON_OVERLAPS when using mariaDB 10.9 $conditions = []; foreach ($query->getValues() as $key => $value) { $conditions[] = "JSON_CONTAINS(`table_main`.{$attribute}, :{$placeholder}_{$key})"; } $condition = implode(' OR ', $conditions); return empty($condition) ? '' : '(' . $condition . ')'; - - -// $conditions = []; -// foreach ($query->getValues() as $key => $value) { -// $conditions[] = "JSON_CONTAINS(`table_main`.{$attribute}, :{$placeholder}_{$key})"; -// } -// $condition = implode(' OR ', $conditions); -// return empty($condition) ? '' : '(' . $condition . ')'; default: $conditions = []; foreach ($query->getValues() as $key => $value) { diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index a413b7774..76a0b7d6e 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1711,10 +1711,22 @@ public function testArrayAttribute(): void $documents = static::getDatabase()->find($collection, [ Query::contains('names', ['Jake', 'Joe']) ]); + $this->assertCount(1, $documents); + + $documents = static::getDatabase()->find($collection, [ + Query::contains('numbers', [-1, 0]) + ]); + $this->assertCount(1, $documents); + + $documents = static::getDatabase()->find($collection, [ + Query::contains('active', [false]) + ]); $this->assertCount(1, $documents); var_dump($documents); + + $this->assertEquals(true,false); } diff --git a/tests/e2e/Adapter/MariaDBTest.php b/tests/e2e/Adapter/MariaDBTest.php index fc3b8ce77..e26ca69c2 100644 --- a/tests/e2e/Adapter/MariaDBTest.php +++ b/tests/e2e/Adapter/MariaDBTest.php @@ -1,60 +1,60 @@ connect('redis', 6379); -// $redis->flushAll(); -// $cache = new Cache(new RedisAdapter($redis)); -// -// $database = new Database(new MariaDB($pdo), $cache); -// $database->setDatabase('utopiaTests'); -// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); -// -// if ($database->exists('utopiaTests')) { -// $database->delete('utopiaTests'); -// } -// -// $database->create(); -// -// return self::$database = $database; -// } -//} + +namespace Tests\E2E\Adapter; + +use PDO; +use Redis; +use Utopia\Cache\Adapter\Redis as RedisAdapter; +use Utopia\Cache\Cache; +use Utopia\Database\Adapter\MariaDB; +use Utopia\Database\Database; + +class MariaDBTest extends Base +{ + protected static ?Database $database = null; + protected static string $namespace; + + // Remove once all methods are implemented + /** + * Return name of adapter + * + * @return string + */ + public static function getAdapterName(): string + { + return "mariadb"; + } + + /** + * @return Database + */ + public static function getDatabase(bool $fresh = false): Database + { + if (!is_null(self::$database) && !$fresh) { + return self::$database; + } + + $dbHost = 'mariadb'; + $dbPort = '3306'; + $dbUser = 'root'; + $dbPass = 'password'; + + $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MariaDB::getPDOAttributes()); + $redis = new Redis(); + $redis->connect('redis', 6379); + $redis->flushAll(); + $cache = new Cache(new RedisAdapter($redis)); + + $database = new Database(new MariaDB($pdo), $cache); + $database->setDatabase('utopiaTests'); + $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + + if ($database->exists('utopiaTests')) { + $database->delete('utopiaTests'); + } + + $database->create(); + + return self::$database = $database; + } +} diff --git a/tests/e2e/Adapter/MySQLTest.php b/tests/e2e/Adapter/MySQLTest.php index d204e8a40..11fcbfb02 100644 --- a/tests/e2e/Adapter/MySQLTest.php +++ b/tests/e2e/Adapter/MySQLTest.php @@ -1,62 +1,62 @@ connect('redis', 6379); - $redis->flushAll(); - - $cache = new Cache(new RedisAdapter($redis)); - - $database = new Database(new MySQL($pdo), $cache); - $database->setDatabase('utopiaTests'); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); - - if ($database->exists('utopiaTests')) { - $database->delete('utopiaTests'); - } - - $database->create(); - - return self::$database = $database; - } -} +// +//namespace Tests\E2E\Adapter; +// +//use PDO; +//use Redis; +//use Utopia\Cache\Adapter\Redis as RedisAdapter; +//use Utopia\Cache\Cache; +//use Utopia\Database\Adapter\MySQL; +//use Utopia\Database\Database; +// +//class MySQLTest extends Base +//{ +// public static ?Database $database = null; +// protected static string $namespace; +// +// // Remove once all methods are implemented +// /** +// * Return name of adapter +// * +// * @return string +// */ +// public static function getAdapterName(): string +// { +// return "mysql"; +// } +// +// /** +// * @return Database +// */ +// public static function getDatabase(): Database +// { +// if (!is_null(self::$database)) { +// return self::$database; +// } +// +// $dbHost = 'mysql'; +// $dbPort = '3307'; +// $dbUser = 'root'; +// $dbPass = 'password'; +// +// $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MySQL::getPDOAttributes()); +// +// $redis = new Redis(); +// $redis->connect('redis', 6379); +// $redis->flushAll(); +// +// $cache = new Cache(new RedisAdapter($redis)); +// +// $database = new Database(new MySQL($pdo), $cache); +// $database->setDatabase('utopiaTests'); +// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); +// +// if ($database->exists('utopiaTests')) { +// $database->delete('utopiaTests'); +// } +// +// $database->create(); +// +// return self::$database = $database; +// } +//} From d4414016a87078e69bba6cab418309daa9557182 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 28 Dec 2023 12:35:36 +0200 Subject: [PATCH 16/52] tests --- tests/e2e/Adapter/Base.php | 5 +- tests/e2e/Adapter/MongoDBTest.php | 192 ++++++++++++++--------------- tests/e2e/Adapter/MySQLTest.php | 122 +++++++++--------- tests/e2e/Adapter/PostgresTest.php | 116 ++++++++--------- tests/e2e/Adapter/SQLiteTest.php | 128 +++++++++---------- 5 files changed, 280 insertions(+), 283 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 76a0b7d6e..6318cc967 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1727,7 +1727,7 @@ public function testArrayAttribute(): void var_dump($documents); - $this->assertEquals(true,false); + //$this->assertEquals(true,false); } } @@ -2121,9 +2121,6 @@ public function testFindContains(): void $this->assertEquals('Invalid query: Cannot query contains on attribute "name" because it is not an array.', $e->getMessage()); $this->assertTrue($e instanceof DatabaseException); } - - $this->assertTrue(false); - } public function testFindFulltext(): void diff --git a/tests/e2e/Adapter/MongoDBTest.php b/tests/e2e/Adapter/MongoDBTest.php index c348bd331..887c26ec2 100644 --- a/tests/e2e/Adapter/MongoDBTest.php +++ b/tests/e2e/Adapter/MongoDBTest.php @@ -1,97 +1,97 @@ connect('redis', 6379); -// $redis->flushAll(); -// $cache = new Cache(new RedisAdapter($redis)); -// -// $schema = 'utopiaTests'; // same as $this->testDatabase -// $client = new Client( -// $schema, -// 'mongo', -// 27017, -// 'root', -// 'example', -// false -// ); -// -// $database = new Database(new Mongo($client), $cache); -// $database->setDatabase($schema); -// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); -// -// if ($database->exists('utopiaTests')) { -// $database->delete('utopiaTests'); -// } -// -// $database->create(); -// -// return self::$database = $database; -// } -// -// /** -// * @throws Exception -// */ -// public function testCreateExistsDelete(): void -// { -// // Mongo creates databases on the fly, so exists would always pass. So we override this test to remove the exists check. -// $this->assertNotNull(static::getDatabase()->create()); -// $this->assertEquals(true, static::getDatabase()->delete($this->testDatabase)); -// $this->assertEquals(true, static::getDatabase()->create()); -// $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase)); -// } -// -// public function testRenameAttribute(): void -// { -// $this->assertTrue(true); -// } -// -// public function testRenameAttributeExisting(): void -// { -// $this->assertTrue(true); -// } -// -// public function testUpdateAttributeStructure(): void -// { -// $this->assertTrue(true); -// } -// -// public function testKeywords(): void -// { -// $this->assertTrue(true); -// } -//} + +namespace Tests\E2E\Adapter; + +use Exception; +use Redis; +use Utopia\Cache\Adapter\Redis as RedisAdapter; +use Utopia\Cache\Cache; +use Utopia\Database\Adapter\Mongo; +use Utopia\Database\Database; +use Utopia\Mongo\Client; + +class MongoDBTest extends Base +{ + public static ?Database $database = null; + protected static string $namespace; + + /** + * Return name of adapter + * + * @return string + */ + public static function getAdapterName(): string + { + return "mongodb"; + } + + /** + * @return Database + * @throws Exception + */ + public static function getDatabase(): Database + { + if (!is_null(self::$database)) { + return self::$database; + } + + $redis = new Redis(); + $redis->connect('redis', 6379); + $redis->flushAll(); + $cache = new Cache(new RedisAdapter($redis)); + + $schema = 'utopiaTests'; // same as $this->testDatabase + $client = new Client( + $schema, + 'mongo', + 27017, + 'root', + 'example', + false + ); + + $database = new Database(new Mongo($client), $cache); + $database->setDatabase($schema); + $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + + if ($database->exists('utopiaTests')) { + $database->delete('utopiaTests'); + } + + $database->create(); + + return self::$database = $database; + } + + /** + * @throws Exception + */ + public function testCreateExistsDelete(): void + { + // Mongo creates databases on the fly, so exists would always pass. So we override this test to remove the exists check. + $this->assertNotNull(static::getDatabase()->create()); + $this->assertEquals(true, static::getDatabase()->delete($this->testDatabase)); + $this->assertEquals(true, static::getDatabase()->create()); + $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase)); + } + + public function testRenameAttribute(): void + { + $this->assertTrue(true); + } + + public function testRenameAttributeExisting(): void + { + $this->assertTrue(true); + } + + public function testUpdateAttributeStructure(): void + { + $this->assertTrue(true); + } + + public function testKeywords(): void + { + $this->assertTrue(true); + } +} diff --git a/tests/e2e/Adapter/MySQLTest.php b/tests/e2e/Adapter/MySQLTest.php index 11fcbfb02..d204e8a40 100644 --- a/tests/e2e/Adapter/MySQLTest.php +++ b/tests/e2e/Adapter/MySQLTest.php @@ -1,62 +1,62 @@ connect('redis', 6379); -// $redis->flushAll(); -// -// $cache = new Cache(new RedisAdapter($redis)); -// -// $database = new Database(new MySQL($pdo), $cache); -// $database->setDatabase('utopiaTests'); -// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); -// -// if ($database->exists('utopiaTests')) { -// $database->delete('utopiaTests'); -// } -// -// $database->create(); -// -// return self::$database = $database; -// } -//} + +namespace Tests\E2E\Adapter; + +use PDO; +use Redis; +use Utopia\Cache\Adapter\Redis as RedisAdapter; +use Utopia\Cache\Cache; +use Utopia\Database\Adapter\MySQL; +use Utopia\Database\Database; + +class MySQLTest extends Base +{ + public static ?Database $database = null; + protected static string $namespace; + + // Remove once all methods are implemented + /** + * Return name of adapter + * + * @return string + */ + public static function getAdapterName(): string + { + return "mysql"; + } + + /** + * @return Database + */ + public static function getDatabase(): Database + { + if (!is_null(self::$database)) { + return self::$database; + } + + $dbHost = 'mysql'; + $dbPort = '3307'; + $dbUser = 'root'; + $dbPass = 'password'; + + $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MySQL::getPDOAttributes()); + + $redis = new Redis(); + $redis->connect('redis', 6379); + $redis->flushAll(); + + $cache = new Cache(new RedisAdapter($redis)); + + $database = new Database(new MySQL($pdo), $cache); + $database->setDatabase('utopiaTests'); + $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + + if ($database->exists('utopiaTests')) { + $database->delete('utopiaTests'); + } + + $database->create(); + + return self::$database = $database; + } +} diff --git a/tests/e2e/Adapter/PostgresTest.php b/tests/e2e/Adapter/PostgresTest.php index 048801724..6a12ecd8c 100644 --- a/tests/e2e/Adapter/PostgresTest.php +++ b/tests/e2e/Adapter/PostgresTest.php @@ -1,59 +1,59 @@ connect('redis', 6379); -// $redis->flushAll(); -// $cache = new Cache(new RedisAdapter($redis)); -// -// $database = new Database(new Postgres($pdo), $cache); -// $database->setDatabase('utopiaTests'); -// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); -// -// if ($database->exists('utopiaTests')) { -// $database->delete('utopiaTests'); -// } -// -// $database->create(); -// -// return self::$database = $database; -// } -//} + +namespace Tests\E2E\Adapter; + +use PDO; +use Redis; +use Utopia\Cache\Adapter\Redis as RedisAdapter; +use Utopia\Cache\Cache; +use Utopia\Database\Adapter\Postgres; +use Utopia\Database\Database; + +class PostgresTest extends Base +{ + public static ?Database $database = null; + protected static string $namespace; + + /** + * Return name of adapter + * + * @return string + */ + public static function getAdapterName(): string + { + return "postgres"; + } + + /** + * @reture Adapter + */ + public static function getDatabase(): Database + { + if (!is_null(self::$database)) { + return self::$database; + } + + $dbHost = 'postgres'; + $dbPort = '5432'; + $dbUser = 'root'; + $dbPass = 'password'; + + $pdo = new PDO("pgsql:host={$dbHost};port={$dbPort};", $dbUser, $dbPass, Postgres::getPDOAttributes()); + $redis = new Redis(); + $redis->connect('redis', 6379); + $redis->flushAll(); + $cache = new Cache(new RedisAdapter($redis)); + + $database = new Database(new Postgres($pdo), $cache); + $database->setDatabase('utopiaTests'); + $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + + if ($database->exists('utopiaTests')) { + $database->delete('utopiaTests'); + } + + $database->create(); + + return self::$database = $database; + } +} diff --git a/tests/e2e/Adapter/SQLiteTest.php b/tests/e2e/Adapter/SQLiteTest.php index 8451dfa91..16c36f6fd 100644 --- a/tests/e2e/Adapter/SQLiteTest.php +++ b/tests/e2e/Adapter/SQLiteTest.php @@ -1,65 +1,65 @@ connect('redis'); -// $redis->flushAll(); -// -// $cache = new Cache(new RedisAdapter($redis)); -// -// $database = new Database(new SQLite($pdo), $cache); -// $database->setDatabase('utopiaTests'); -// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); -// -// if ($database->exists()) { -// $database->delete(); -// } -// -// $database->create(); -// -// return self::$database = $database; -// } -//} + +namespace Tests\E2E\Adapter; + +use PDO; +use Redis; +use Utopia\Cache\Adapter\Redis as RedisAdapter; +use Utopia\Cache\Cache; +use Utopia\Database\Adapter\SQLite; +use Utopia\Database\Database; + +class SQLiteTest extends Base +{ + public static ?Database $database = null; + protected static string $namespace; + + // Remove once all methods are implemented + /** + * Return name of adapter + * + * @return string + */ + public static function getAdapterName(): string + { + return "sqlite"; + } + + /** + * @return Database + */ + public static function getDatabase(): Database + { + if (!is_null(self::$database)) { + return self::$database; + } + + $db = __DIR__."/database.sql"; + + if (file_exists($db)) { + unlink($db); + } + + $dsn = $db; + //$dsn = 'memory'; // Overwrite for fast tests + $pdo = new PDO("sqlite:" . $dsn, null, null, SQLite::getPDOAttributes()); + + $redis = new Redis(); + $redis->connect('redis'); + $redis->flushAll(); + + $cache = new Cache(new RedisAdapter($redis)); + + $database = new Database(new SQLite($pdo), $cache); + $database->setDatabase('utopiaTests'); + $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + + if ($database->exists()) { + $database->delete(); + } + + $database->create(); + + return self::$database = $database; + } +} From 5a3d56fdc53503425b87dfcfa53a36aa12392db2 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 31 Dec 2023 11:14:06 +0200 Subject: [PATCH 17/52] createIndex order test --- tests/e2e/Adapter/Base.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 6318cc967..c0d4c584c 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1623,7 +1623,7 @@ public function testArrayAttribute(): void $collection, 'names', Database::VAR_STRING, - size: 255, + size: 255, // todo: this makes problems, array is Longtext/Json while length is data length, index problems required: false, array: true )); @@ -1705,7 +1705,8 @@ public function testArrayAttribute(): void $this->assertTrue(true, static::getDatabase()->createIndex($collection, 'indx-numbers', Database::INDEX_ARRAY, ['numbers'], [], [])); - //$this->assertTrue(true, static::getDatabase()->createIndex($collection, 'indx-names', Database::INDEX_ARRAY, ['age', 'names'], [], ['asc', 'desc'])); + $this->assertTrue(true, static::getDatabase()->createIndex($collection, 'indx-names1', Database::INDEX_ARRAY, ['names'], [255], ['desc'])); + $this->assertTrue(true, static::getDatabase()->createIndex($collection, 'indx-names2', Database::INDEX_ARRAY, ['age', 'names'], [100,100], ['asc', 'desc'])); if ($this->getDatabase()->getAdapter()->getSupportForQueryContains()) { $documents = static::getDatabase()->find($collection, [ @@ -1725,11 +1726,10 @@ public function testArrayAttribute(): void $this->assertCount(1, $documents); var_dump($documents); - - - //$this->assertEquals(true,false); } + $this->assertEquals(true,false); + } /** From cc22abd26f4920f705497a39dfc110418def505f Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 3 Jan 2024 19:17:27 +0200 Subject: [PATCH 18/52] Order tests --- docker-compose.yml | 2 +- src/Database/Adapter/MariaDB.php | 4 - src/Database/Database.php | 2 +- src/Database/Validator/Index.php | 57 ++++++++- tests/e2e/Adapter/Base.php | 98 ++++++++++++--- tests/e2e/Adapter/MongoDBTest.php | 192 ++++++++++++++--------------- tests/e2e/Adapter/MySQLTest.php | 122 +++++++++--------- tests/e2e/Adapter/PostgresTest.php | 116 ++++++++--------- 8 files changed, 351 insertions(+), 242 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8b386c771..518c145f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,7 +38,7 @@ services: POSTGRES_PASSWORD: password mariadb: - image: mariadb:10.7 + image: mariadb:10.9 # Is it ok to upgrade Appwrite to >= 10.9 for JSON_OVERLAPS container_name: utopia-mariadb networks: - database diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 6aed86337..3a893be49 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -272,10 +272,6 @@ public function createAttribute(string $collection, string $id, string $type, in $id = $this->filter($id); $type = $this->getSQLType($type, $size, $signed, $array); - if ($array) { - $type = 'JSON'; - } - $sql = "ALTER TABLE {$this->getSQLTable($name)} ADD COLUMN `{$id}` {$type};"; $sql = $this->trigger(Database::EVENT_ATTRIBUTE_CREATE, $sql); diff --git a/src/Database/Database.php b/src/Database/Database.php index 3f8f133bb..22d5659d1 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2362,7 +2362,7 @@ public function createIndex(string $collection, string $id, string $type, array throw new DatabaseException('Fulltext index is not supported'); } break; - + case self::INDEX_ARRAY: if (!$this->adapter->getSupportForIndexArray()) { throw new DatabaseException('Key index array is not supported'); diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 6cdf9d89c..d1bfffd52 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -20,6 +20,7 @@ class Index extends Validator /** * @param array $attributes * @param int $maxLength + * @param bool $indexArraySupport * @throws DatabaseException */ public function __construct(array $attributes, int $maxLength) @@ -97,7 +98,6 @@ public function checkDuplicatedAttributes(Document $index): bool /** * @param Document $index * @return bool - * @throws DatabaseException */ public function checkFulltextIndexNonString(Document $index): bool { @@ -113,6 +113,42 @@ public function checkFulltextIndexNonString(Document $index): bool return true; } + /** + * @param Document $index + * @return bool + */ + public function checkArrayIndex(Document $index): bool + { + $attributes = $index->getAttribute('attributes', []); + $orders = $index->getAttribute('orders', []); + + $arrayAttributes = []; + foreach ($attributes as $key => $attribute) { + $attribute = $this->attributes[\strtolower($attribute)] ?? new Document(); + if($attribute->getAttribute('array') === true){ + // Database::INDEX_UNIQUE Is not allowed! since mariaDB VS MySQL makes the unique Different on values + if(!in_array($index->getAttribute('type'), [Database::INDEX_ARRAY, Database::INDEX_KEY])){ + $this->message = 'Invalid "' . ucfirst($index->getAttribute('type')) . '" index on array attributes'; + return false; + } + + var_dump($attribute); + $arrayAttributes[] = $attribute->getAttribute('key', ''); + if(count($arrayAttributes) > 1){ + $this->message = 'Only a single index can be created on array attributes found "' . implode(',', $arrayAttributes) . '"'; + return false; + } + + $direction = $orders[$key] ?? ''; + if(!empty($direction)){ + $this->message = 'Invalid index order "' . $direction . '" on array attribute "'. $attribute->getAttribute('key', '') .'"'; + return false; + } + } + } + return true; + } + /** * @param Document $index * @return bool @@ -129,10 +165,24 @@ public function checkIndexLength(Document $index): bool foreach ($index->getAttribute('attributes', []) as $attributePosition => $attributeName) { $attribute = $this->attributes[\strtolower($attributeName)]; + $isArray = $attribute->getAttribute('array', false); + + if($isArray && empty($lengths[$attributePosition])){ + $this->message = 'Index length for array not specified'; + return false; + } + + if(!$isArray && $attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])){ + $this->message = 'Key part length are forbidden on "' . $attribute->getAttribute('type') . '" data-type'; + return false; + } + switch ($attribute->getAttribute('type')) { case Database::VAR_STRING: $attributeSize = $attribute->getAttribute('size', 0); $indexLength = $lengths[$attributePosition] ?? $attributeSize; + var_dump($attributeName); + var_dump($indexLength); break; case Database::VAR_FLOAT: $attributeSize = 2; // 8 bytes / 4 mb4 @@ -140,6 +190,7 @@ public function checkIndexLength(Document $index): bool break; default: $attributeSize = 1; // 4 bytes / 4 mb4 + // $attributeSize = $attribute->getAttribute('size', 1); // 4 bytes / 4 mb4 $indexLength = 1; break; } @@ -186,6 +237,10 @@ public function isValid($value): bool return false; } + if (!$this->checkArrayIndex($value)) { + return false; + } + if (!$this->checkIndexLength($value)) { return false; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index c0d4c584c..6fc337fd1 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1597,7 +1597,6 @@ public function testDeleteDocument(Document $document): void * @throws ConflictException * @throws LimitException * @throws StructureException - * @throws DatabaseException */ public function testArrayAttribute(): void { @@ -1612,7 +1611,7 @@ public function testArrayAttribute(): void $this->assertEquals(true, static::getDatabase()->createAttribute( $collection, - 'active', + 'booleans', Database::VAR_BOOLEAN, size: 0, required: true, @@ -1623,7 +1622,7 @@ public function testArrayAttribute(): void $collection, 'names', Database::VAR_STRING, - size: 255, // todo: this makes problems, array is Longtext/Json while length is data length, index problems + size: 255, // Does this mean each Element max is 255? We need to check this on Structure validation? required: false, array: true )); @@ -1651,12 +1650,12 @@ public function testArrayAttribute(): void static::getDatabase()->createDocument($collection, new Document([])); $this->fail('Failed to throw exception'); } catch(Throwable $e) { - $this->assertEquals('Invalid document structure: Missing required attribute "active"', $e->getMessage()); + $this->assertEquals('Invalid document structure: Missing required attribute "booleans"', $e->getMessage()); } try { static::getDatabase()->createDocument($collection, new Document([ - 'active' => [false], + 'booleans' => [false], 'names' => ['Joe', 100], ])); $this->fail('Failed to throw exception'); @@ -1666,7 +1665,7 @@ public function testArrayAttribute(): void try { static::getDatabase()->createDocument($collection, new Document([ - 'active' => [false], + 'booleans' => [false], 'age' => 1.5, ])); $this->fail('Failed to throw exception'); @@ -1676,7 +1675,7 @@ public function testArrayAttribute(): void try { static::getDatabase()->createDocument($collection, new Document([ - 'active' => [false], + 'booleans' => [false], 'age' => -1, ])); $this->fail('Failed to throw exception'); @@ -1684,29 +1683,88 @@ public function testArrayAttribute(): void //$this->assertEquals('Should fail since it is Signed = false!!!!', $e->getMessage()); } - $document = static::getDatabase()->createDocument($collection, new Document([ + static::getDatabase()->createDocument($collection, new Document([ + '$id' => 'joe', '$permissions' => $permissions, - 'active' => [false], + 'booleans' => [false], 'names' => ['Joe', 'Antony', '100'], 'numbers' => [0, 100, 1000, -1], ])); - $this->assertEquals(false, $document->getAttribute('active')[0]); + $document = static::getDatabase()->getDocument($collection, 'joe'); + + $this->assertEquals(false, $document->getAttribute('booleans')[0]); $this->assertEquals('Antony', $document->getAttribute('names')[1]); $this->assertEquals(100, $document->getAttribute('numbers')[1]); -// try { -// todo: force create only INDEX_ARRAY for array???? -// static::getDatabase()->createIndex($collection, 'ind-names', Database::INDEX_FULLTEXT, ['names']); -// $this->fail('Failed to throw exception'); -// } catch(Throwable $e) { -// $this->assertEquals('Should this fail? can we create a fulltext index on array as users do today?', $e->getMessage()); -// } + try { + static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_FULLTEXT, ['names']); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + $this->assertEquals('Invalid "Fulltext" index on array attributes', $e->getMessage()); + } + + try { + static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['numbers', 'names']); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + $this->assertEquals('Only a single index can be created on array attributes found "numbers,names"', $e->getMessage()); + } + + try { + static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['numbers', 'names']); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + $this->assertEquals('Only a single index can be created on array attributes found "numbers,names"', $e->getMessage()); + } + + + $this->assertEquals(true, static::getDatabase()->createAttribute( + $collection, + 'long_names', + Database::VAR_STRING, + size: 2000, + required: false, + array: true + )); + + try { + static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['long_names'], [], []); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + $this->assertEquals('Index length for array not specified', $e->getMessage()); + } + + try { + static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['long_names'], [1000], []); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + $this->assertEquals('Index length is longer than the maximum: 768', $e->getMessage()); + } + + try { + static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['names'], [255], ['desc']); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + $this->assertEquals('Invalid index order "desc" on array attribute "names"', $e->getMessage()); + } + + try { + static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['age', 'names'], [10, 255], []); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + $this->assertEquals('Key part length are forbidden on "integer" data-type', $e->getMessage()); + } + + $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_names', Database::INDEX_KEY, ['names'], [255], [])); + $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_age_names1', Database::INDEX_KEY, ['age', 'names'], [null, 255], [])); + $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_age_names2', Database::INDEX_KEY, ['age', 'booleans'], [0, 255], [])); + // $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_age_names', Database::INDEX_ARRAY, ['age', 'names'], [255, 255], [])); + + + $this->assertEquals(true,false); - $this->assertTrue(true, static::getDatabase()->createIndex($collection, 'indx-numbers', Database::INDEX_ARRAY, ['numbers'], [], [])); - $this->assertTrue(true, static::getDatabase()->createIndex($collection, 'indx-names1', Database::INDEX_ARRAY, ['names'], [255], ['desc'])); - $this->assertTrue(true, static::getDatabase()->createIndex($collection, 'indx-names2', Database::INDEX_ARRAY, ['age', 'names'], [100,100], ['asc', 'desc'])); if ($this->getDatabase()->getAdapter()->getSupportForQueryContains()) { $documents = static::getDatabase()->find($collection, [ diff --git a/tests/e2e/Adapter/MongoDBTest.php b/tests/e2e/Adapter/MongoDBTest.php index 887c26ec2..c348bd331 100644 --- a/tests/e2e/Adapter/MongoDBTest.php +++ b/tests/e2e/Adapter/MongoDBTest.php @@ -1,97 +1,97 @@ connect('redis', 6379); - $redis->flushAll(); - $cache = new Cache(new RedisAdapter($redis)); - - $schema = 'utopiaTests'; // same as $this->testDatabase - $client = new Client( - $schema, - 'mongo', - 27017, - 'root', - 'example', - false - ); - - $database = new Database(new Mongo($client), $cache); - $database->setDatabase($schema); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); - - if ($database->exists('utopiaTests')) { - $database->delete('utopiaTests'); - } - - $database->create(); - - return self::$database = $database; - } - - /** - * @throws Exception - */ - public function testCreateExistsDelete(): void - { - // Mongo creates databases on the fly, so exists would always pass. So we override this test to remove the exists check. - $this->assertNotNull(static::getDatabase()->create()); - $this->assertEquals(true, static::getDatabase()->delete($this->testDatabase)); - $this->assertEquals(true, static::getDatabase()->create()); - $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase)); - } - - public function testRenameAttribute(): void - { - $this->assertTrue(true); - } - - public function testRenameAttributeExisting(): void - { - $this->assertTrue(true); - } - - public function testUpdateAttributeStructure(): void - { - $this->assertTrue(true); - } - - public function testKeywords(): void - { - $this->assertTrue(true); - } -} +// +//namespace Tests\E2E\Adapter; +// +//use Exception; +//use Redis; +//use Utopia\Cache\Adapter\Redis as RedisAdapter; +//use Utopia\Cache\Cache; +//use Utopia\Database\Adapter\Mongo; +//use Utopia\Database\Database; +//use Utopia\Mongo\Client; +// +//class MongoDBTest extends Base +//{ +// public static ?Database $database = null; +// protected static string $namespace; +// +// /** +// * Return name of adapter +// * +// * @return string +// */ +// public static function getAdapterName(): string +// { +// return "mongodb"; +// } +// +// /** +// * @return Database +// * @throws Exception +// */ +// public static function getDatabase(): Database +// { +// if (!is_null(self::$database)) { +// return self::$database; +// } +// +// $redis = new Redis(); +// $redis->connect('redis', 6379); +// $redis->flushAll(); +// $cache = new Cache(new RedisAdapter($redis)); +// +// $schema = 'utopiaTests'; // same as $this->testDatabase +// $client = new Client( +// $schema, +// 'mongo', +// 27017, +// 'root', +// 'example', +// false +// ); +// +// $database = new Database(new Mongo($client), $cache); +// $database->setDatabase($schema); +// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); +// +// if ($database->exists('utopiaTests')) { +// $database->delete('utopiaTests'); +// } +// +// $database->create(); +// +// return self::$database = $database; +// } +// +// /** +// * @throws Exception +// */ +// public function testCreateExistsDelete(): void +// { +// // Mongo creates databases on the fly, so exists would always pass. So we override this test to remove the exists check. +// $this->assertNotNull(static::getDatabase()->create()); +// $this->assertEquals(true, static::getDatabase()->delete($this->testDatabase)); +// $this->assertEquals(true, static::getDatabase()->create()); +// $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase)); +// } +// +// public function testRenameAttribute(): void +// { +// $this->assertTrue(true); +// } +// +// public function testRenameAttributeExisting(): void +// { +// $this->assertTrue(true); +// } +// +// public function testUpdateAttributeStructure(): void +// { +// $this->assertTrue(true); +// } +// +// public function testKeywords(): void +// { +// $this->assertTrue(true); +// } +//} diff --git a/tests/e2e/Adapter/MySQLTest.php b/tests/e2e/Adapter/MySQLTest.php index d204e8a40..11fcbfb02 100644 --- a/tests/e2e/Adapter/MySQLTest.php +++ b/tests/e2e/Adapter/MySQLTest.php @@ -1,62 +1,62 @@ connect('redis', 6379); - $redis->flushAll(); - - $cache = new Cache(new RedisAdapter($redis)); - - $database = new Database(new MySQL($pdo), $cache); - $database->setDatabase('utopiaTests'); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); - - if ($database->exists('utopiaTests')) { - $database->delete('utopiaTests'); - } - - $database->create(); - - return self::$database = $database; - } -} +// +//namespace Tests\E2E\Adapter; +// +//use PDO; +//use Redis; +//use Utopia\Cache\Adapter\Redis as RedisAdapter; +//use Utopia\Cache\Cache; +//use Utopia\Database\Adapter\MySQL; +//use Utopia\Database\Database; +// +//class MySQLTest extends Base +//{ +// public static ?Database $database = null; +// protected static string $namespace; +// +// // Remove once all methods are implemented +// /** +// * Return name of adapter +// * +// * @return string +// */ +// public static function getAdapterName(): string +// { +// return "mysql"; +// } +// +// /** +// * @return Database +// */ +// public static function getDatabase(): Database +// { +// if (!is_null(self::$database)) { +// return self::$database; +// } +// +// $dbHost = 'mysql'; +// $dbPort = '3307'; +// $dbUser = 'root'; +// $dbPass = 'password'; +// +// $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MySQL::getPDOAttributes()); +// +// $redis = new Redis(); +// $redis->connect('redis', 6379); +// $redis->flushAll(); +// +// $cache = new Cache(new RedisAdapter($redis)); +// +// $database = new Database(new MySQL($pdo), $cache); +// $database->setDatabase('utopiaTests'); +// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); +// +// if ($database->exists('utopiaTests')) { +// $database->delete('utopiaTests'); +// } +// +// $database->create(); +// +// return self::$database = $database; +// } +//} diff --git a/tests/e2e/Adapter/PostgresTest.php b/tests/e2e/Adapter/PostgresTest.php index 6a12ecd8c..048801724 100644 --- a/tests/e2e/Adapter/PostgresTest.php +++ b/tests/e2e/Adapter/PostgresTest.php @@ -1,59 +1,59 @@ connect('redis', 6379); - $redis->flushAll(); - $cache = new Cache(new RedisAdapter($redis)); - - $database = new Database(new Postgres($pdo), $cache); - $database->setDatabase('utopiaTests'); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); - - if ($database->exists('utopiaTests')) { - $database->delete('utopiaTests'); - } - - $database->create(); - - return self::$database = $database; - } -} +// +//namespace Tests\E2E\Adapter; +// +//use PDO; +//use Redis; +//use Utopia\Cache\Adapter\Redis as RedisAdapter; +//use Utopia\Cache\Cache; +//use Utopia\Database\Adapter\Postgres; +//use Utopia\Database\Database; +// +//class PostgresTest extends Base +//{ +// public static ?Database $database = null; +// protected static string $namespace; +// +// /** +// * Return name of adapter +// * +// * @return string +// */ +// public static function getAdapterName(): string +// { +// return "postgres"; +// } +// +// /** +// * @reture Adapter +// */ +// public static function getDatabase(): Database +// { +// if (!is_null(self::$database)) { +// return self::$database; +// } +// +// $dbHost = 'postgres'; +// $dbPort = '5432'; +// $dbUser = 'root'; +// $dbPass = 'password'; +// +// $pdo = new PDO("pgsql:host={$dbHost};port={$dbPort};", $dbUser, $dbPass, Postgres::getPDOAttributes()); +// $redis = new Redis(); +// $redis->connect('redis', 6379); +// $redis->flushAll(); +// $cache = new Cache(new RedisAdapter($redis)); +// +// $database = new Database(new Postgres($pdo), $cache); +// $database->setDatabase('utopiaTests'); +// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); +// +// if ($database->exists('utopiaTests')) { +// $database->delete('utopiaTests'); +// } +// +// $database->create(); +// +// return self::$database = $database; +// } +//} From 9846af4836f54c9e47eac11d91dc11252d81860a Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 4 Jan 2024 10:53:22 +0200 Subject: [PATCH 19/52] Order tests --- src/Database/Validator/Index.php | 10 ++++------ tests/e2e/Adapter/Base.php | 20 ++++---------------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index d1bfffd52..f43a2b6c7 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -125,14 +125,15 @@ public function checkArrayIndex(Document $index): bool $arrayAttributes = []; foreach ($attributes as $key => $attribute) { $attribute = $this->attributes[\strtolower($attribute)] ?? new Document(); + var_dump($attribute); + if($attribute->getAttribute('array') === true){ // Database::INDEX_UNIQUE Is not allowed! since mariaDB VS MySQL makes the unique Different on values if(!in_array($index->getAttribute('type'), [Database::INDEX_ARRAY, Database::INDEX_KEY])){ - $this->message = 'Invalid "' . ucfirst($index->getAttribute('type')) . '" index on array attributes'; + $this->message = ucfirst($index->getAttribute('type')) . '" index is forbidden on array attributes'; return false; } - var_dump($attribute); $arrayAttributes[] = $attribute->getAttribute('key', ''); if(count($arrayAttributes) > 1){ $this->message = 'Only a single index can be created on array attributes found "' . implode(',', $arrayAttributes) . '"'; @@ -173,7 +174,7 @@ public function checkIndexLength(Document $index): bool } if(!$isArray && $attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])){ - $this->message = 'Key part length are forbidden on "' . $attribute->getAttribute('type') . '" data-type'; + $this->message = 'Key part length are forbidden on "' . $attribute->getAttribute('type') . '" data-type for "' . $attributeName . '"'; return false; } @@ -181,8 +182,6 @@ public function checkIndexLength(Document $index): bool case Database::VAR_STRING: $attributeSize = $attribute->getAttribute('size', 0); $indexLength = $lengths[$attributePosition] ?? $attributeSize; - var_dump($attributeName); - var_dump($indexLength); break; case Database::VAR_FLOAT: $attributeSize = 2; // 8 bytes / 4 mb4 @@ -190,7 +189,6 @@ public function checkIndexLength(Document $index): bool break; default: $attributeSize = 1; // 4 bytes / 4 mb4 - // $attributeSize = $attribute->getAttribute('size', 1); // 4 bytes / 4 mb4 $indexLength = 1; break; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 6fc337fd1..dd7d47843 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1673,16 +1673,6 @@ public function testArrayAttribute(): void $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid integer', $e->getMessage()); } - try { - static::getDatabase()->createDocument($collection, new Document([ - 'booleans' => [false], - 'age' => -1, - ])); - $this->fail('Failed to throw exception'); - } catch(Throwable $e) { - //$this->assertEquals('Should fail since it is Signed = false!!!!', $e->getMessage()); - } - static::getDatabase()->createDocument($collection, new Document([ '$id' => 'joe', '$permissions' => $permissions, @@ -1701,7 +1691,7 @@ public function testArrayAttribute(): void static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_FULLTEXT, ['names']); $this->fail('Failed to throw exception'); } catch(Throwable $e) { - $this->assertEquals('Invalid "Fulltext" index on array attributes', $e->getMessage()); + $this->assertEquals('Fulltext" index is forbidden on array attributes', $e->getMessage()); } try { @@ -1753,17 +1743,17 @@ public function testArrayAttribute(): void static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['age', 'names'], [10, 255], []); $this->fail('Failed to throw exception'); } catch(Throwable $e) { - $this->assertEquals('Key part length are forbidden on "integer" data-type', $e->getMessage()); + $this->assertEquals('Key part length are forbidden on "integer" data-type for "age"', $e->getMessage()); } $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_names', Database::INDEX_KEY, ['names'], [255], [])); $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_age_names1', Database::INDEX_KEY, ['age', 'names'], [null, 255], [])); $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_age_names2', Database::INDEX_KEY, ['age', 'booleans'], [0, 255], [])); - // $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_age_names', Database::INDEX_ARRAY, ['age', 'names'], [255, 255], [])); - $this->assertEquals(true,false); + $this->assertTrue(static::getDatabase()->createIndex($collection, 'test', Database::INDEX_ARRAY, ['names'], [255], [])); + $this->assertEquals(true,false); if ($this->getDatabase()->getAdapter()->getSupportForQueryContains()) { @@ -1777,7 +1767,6 @@ public function testArrayAttribute(): void ]); $this->assertCount(1, $documents); - $documents = static::getDatabase()->find($collection, [ Query::contains('active', [false]) ]); @@ -1787,7 +1776,6 @@ public function testArrayAttribute(): void } $this->assertEquals(true,false); - } /** From 619f797f9b2766a0ed7082ee23008431aac330a2 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 4 Jan 2024 13:32:05 +0200 Subject: [PATCH 20/52] Signature change create Index --- src/Database/Adapter.php | 7 +- src/Database/Adapter/MariaDB.php | 49 ++++++----- src/Database/Adapter/Mongo.php | 19 +++-- src/Database/Adapter/Postgres.php | 71 +++++++++------- src/Database/Adapter/SQLite.php | 7 +- src/Database/Database.php | 10 ++- src/Database/Validator/Index.php | 1 - tests/e2e/Adapter/Base.php | 12 +-- tests/e2e/Adapter/PostgresTest.php | 116 +++++++++++++------------- tests/e2e/Adapter/SQLiteTest.php | 128 ++++++++++++++--------------- 10 files changed, 217 insertions(+), 203 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index d50d7d8df..beb0ec866 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -430,13 +430,10 @@ abstract public function renameIndex(string $collection, string $old, string $ne * @param string $collection * @param string $id * @param string $type - * @param array $attributes - * @param array $lengths - * @param array $orders - * + * @param array $attributes * @return bool */ - abstract public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders): bool; + abstract public function createIndex(string $collection, string $id, string $type, array $attributes): bool; /** * Delete Index diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 3a893be49..197b62fd1 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -604,39 +604,44 @@ public function renameIndex(string $collection, string $old, string $new): bool * @param string $collection * @param string $id * @param string $type - * @param array $attributes - * @param array $lengths - * @param array $orders + * @param array $attributes * @return bool - * @throws Exception - * @throws PDOException + * @throws DatabaseException */ - public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders): bool + public function createIndex(string $collection, string $id, string $type, array $attributes): bool { $name = $this->filter($collection); $id = $this->filter($id); - $attributes = \array_map(fn ($attribute) => match ($attribute) { - '$id' => '_uid', - '$createdAt' => '_createdAt', - '$updatedAt' => '_updatedAt', - default => $attribute - }, $attributes); - foreach ($attributes as $key => $attribute) { - $length = $lengths[$key] ?? ''; - $length = (empty($length)) ? '' : '(' . (int)$length . ')'; - $order = $orders[$key] ?? ''; - $attribute = $this->filter($attribute); + $length = empty($attribute['length']) ? '' : '(' . (int)$attribute['length'] . ')'; + $order = empty($attribute['order']) || Database::INDEX_FULLTEXT === $type ? '' : $attribute['order']; + + $attr = match ($attribute['attribute']) { + '$id' => '_uid', + '$createdAt' => '_createdAt', + '$updatedAt' => '_updatedAt', + default => $this->filter($attribute['attribute']), + }; - if (Database::INDEX_FULLTEXT === $type) { - $order = ''; - } + $attributes[$key] = "`{$attr}`{$length} {$order}"; + } + + $sqlType = match ($type) { + Database::INDEX_KEY => 'INDEX', + Database::INDEX_UNIQUE => 'UNIQUE INDEX', + Database::INDEX_FULLTEXT => 'FULLTEXT INDEX', + default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT), + }; - $attributes[$key] = "`{$attribute}`{$length} {$order}"; + $attributes = \implode(', ', $attributes); + + if ($this->shareTables && $type !== Database::INDEX_FULLTEXT) { + // Add tenant as first index column for best performance + $attributes = "_tenant, {$attributes}"; } - $sql = $this->getSQLIndex($name, $id, $type, $attributes); + $sql = "CREATE {$sqlType} `{$id}` ON {$this->getSQLTable($name)} ({$attributes})"; var_dump($sql); $sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql); diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 9be2b0429..38247e47e 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -509,14 +509,14 @@ public function deleteRelationship( * @param string $collection * @param string $id * @param string $type - * @param array $attributes - * @param array $lengths - * @param array $orders + * @param array $attributes * @param array $collation * @return bool + * @throws DatabaseException + * @throws MongoException * @throws Exception */ - public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $collation = []): bool + public function createIndex(string $collection, string $id, string $type, array $attributes, array $collation = []): bool { $name = $this->getNamespace() . '_' . $this->filter($collection); $id = $this->filter($id); @@ -527,11 +527,14 @@ public function createIndex(string $collection, string $id, string $type, array // pass in custom index name $indexes['name'] = $id; - foreach ($attributes as $i => $attribute) { - $attribute = $this->filter($attribute); + foreach ($attributes as $attribute) { + + $attr = $attribute['attribute']; + $attr = $this->filter($attr); - $orderType = $this->getOrder($this->filter($orders[$i] ?? Database::ORDER_ASC)); - $indexes['key'][$attribute] = $orderType; + $order = $this->filter($attribute['order']); + $order = empty($order) ? Database::ORDER_ASC : $order; + $indexes['key'][$attr] = $this->getOrder($order); switch ($type) { case Database::INDEX_KEY: diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 18cc8acb7..06deba8cd 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -84,7 +84,7 @@ public function createCollection(string $name, array $attributes = [], array $in $this->getPDO()->beginTransaction(); - foreach ($attributes as &$attribute) { + foreach ($attributes as $index => $attribute) { $attrId = $this->filter($attribute->getId()); $attrType = $this->getSQLType( @@ -94,7 +94,7 @@ public function createCollection(string $name, array $attributes = [], array $in $attribute->getAttribute('array', false) ); - $attribute = "\"{$attrId}\" {$attrType}, "; + $attributes[$index] = "\"{$attrId}\" {$attrType}, "; } /** @@ -171,16 +171,22 @@ public function createCollection(string $name, array $attributes = [], array $in foreach ($indexes as $index) { $indexId = $this->filter($index->getId()); $indexType = $index->getAttribute('type'); - $indexAttributes = $index->getAttribute('attributes'); $indexOrders = $index->getAttribute('orders', []); + $indexAttributes = $index->getAttribute('attributes', []); + + foreach ($indexAttributes as $key => $attribute){ + $indexAttributes[$key] = [ + 'attribute' => $attribute, + 'order' => $indexOrders[$key] ?? null, + 'length' => null, + ]; + } $this->createIndex( $id, $indexId, $indexType, $indexAttributes, - [], - $indexOrders ); } } catch (Exception $e) { @@ -571,42 +577,49 @@ public function deleteRelationship( * @param string $collection * @param string $id * @param string $type - * @param array $attributes - * @param array $lengths - * @param array $orders - * + * @param array $attributes * @return bool + * @throws DatabaseException */ - public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders): bool + public function createIndex(string $collection, string $id, string $type, array $attributes): bool { - $name = $this->filter($collection); + $collection = $this->filter($collection); $id = $this->filter($id); - $attributes = \array_map(fn ($attribute) => match ($attribute) { - '$id' => '_uid', - '$createdAt' => '_createdAt', - '$updatedAt' => '_updatedAt', - default => $attribute - }, $attributes); - - foreach ($attributes as $key => &$attribute) { - $length = $lengths[$key] ?? ''; - $length = (empty($length)) ? '' : '(' . (int)$length . ')'; - $order = $orders[$key] ?? ''; - $attribute = $this->filter($attribute); + foreach ($attributes as $key => $attribute) { + $order = empty($attribute['order']) || Database::INDEX_FULLTEXT === $type ? '' : $attribute['order']; - if (Database::INDEX_FULLTEXT === $type) { - $order = ''; - } + $attr = match ($attribute['attribute']) { + '$id' => '_uid', + '$createdAt' => '_createdAt', + '$updatedAt' => '_updatedAt', + default => $this->filter($attribute['attribute']), + }; if (Database::INDEX_UNIQUE === $type) { - $attribute = "LOWER(\"{$attribute}\"::text) {$order}"; + $attributes[$key] = "LOWER(\"{$attr}\"::text) {$order}"; } else { - $attribute = "\"{$attribute}\" {$order}"; + $attributes[$key] = "\"{$attr}\" {$order}"; } } - $sql = $this->getSQLIndex($name, $id, $type, $attributes); + $sqlType = match ($type) { + Database::INDEX_KEY, + Database::INDEX_FULLTEXT => 'INDEX', + Database::INDEX_UNIQUE => 'UNIQUE INDEX', + default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT), + }; + + $key = "\"{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}\""; + $attributes = \implode(', ', $attributes); + + if ($this->shareTables && $type !== Database::INDEX_FULLTEXT) { + // Add tenant as first index column for best performance + $attributes = "_tenant, {$attributes}"; + } + + $sql = "CREATE {$sqlType} {$key} ON {$this->getSQLTable($collection)} ({$attributes});"; + var_dump($sql); $sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql); return $this->getPDO() diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index b8b9b9085..3da5ea5af 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -377,13 +377,10 @@ public function renameIndex(string $collection, string $old, string $new): bool * @param string $id * @param string $type * @param array $attributes - * @param array $lengths - * @param array $orders * @return bool - * @throws Exception - * @throws PDOException + * @throws DatabaseException */ - public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders): bool + public function createIndex(string $collection, string $id, string $type, array $attributes): bool { $name = $this->filter($collection); $id = $this->filter($id); diff --git a/src/Database/Database.php b/src/Database/Database.php index 22d5659d1..722424f64 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2393,7 +2393,15 @@ public function createIndex(string $collection, string $id, string $type, array } } - $index = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes, $lengths, $orders); + foreach ($attributes as $key => $attribute){ + $attributes[$key] = [ + 'attribute' => $attribute, + 'order' => $orders[$key] ?? null, + 'length' => $lengths[$key] ?? null, + ]; + } + + $index = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes); if ($collection->getId() !== self::METADATA) { $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection)); diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index f43a2b6c7..2ed0af009 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -125,7 +125,6 @@ public function checkArrayIndex(Document $index): bool $arrayAttributes = []; foreach ($attributes as $key => $attribute) { $attribute = $this->attributes[\strtolower($attribute)] ?? new Document(); - var_dump($attribute); if($attribute->getAttribute('array') === true){ // Database::INDEX_UNIQUE Is not allowed! since mariaDB VS MySQL makes the unique Different on values diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index dd7d47843..29d43e27a 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1750,12 +1750,6 @@ public function testArrayAttribute(): void $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_age_names1', Database::INDEX_KEY, ['age', 'names'], [null, 255], [])); $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_age_names2', Database::INDEX_KEY, ['age', 'booleans'], [0, 255], [])); - - $this->assertTrue(static::getDatabase()->createIndex($collection, 'test', Database::INDEX_ARRAY, ['names'], [255], [])); - - $this->assertEquals(true,false); - - if ($this->getDatabase()->getAdapter()->getSupportForQueryContains()) { $documents = static::getDatabase()->find($collection, [ Query::contains('names', ['Jake', 'Joe']) @@ -1768,14 +1762,12 @@ public function testArrayAttribute(): void $this->assertCount(1, $documents); $documents = static::getDatabase()->find($collection, [ - Query::contains('active', [false]) + Query::contains('booleans', [false]) ]); $this->assertCount(1, $documents); - - var_dump($documents); } - $this->assertEquals(true,false); + //$this->assertEquals(true,false); } /** diff --git a/tests/e2e/Adapter/PostgresTest.php b/tests/e2e/Adapter/PostgresTest.php index 048801724..6a12ecd8c 100644 --- a/tests/e2e/Adapter/PostgresTest.php +++ b/tests/e2e/Adapter/PostgresTest.php @@ -1,59 +1,59 @@ connect('redis', 6379); -// $redis->flushAll(); -// $cache = new Cache(new RedisAdapter($redis)); -// -// $database = new Database(new Postgres($pdo), $cache); -// $database->setDatabase('utopiaTests'); -// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); -// -// if ($database->exists('utopiaTests')) { -// $database->delete('utopiaTests'); -// } -// -// $database->create(); -// -// return self::$database = $database; -// } -//} + +namespace Tests\E2E\Adapter; + +use PDO; +use Redis; +use Utopia\Cache\Adapter\Redis as RedisAdapter; +use Utopia\Cache\Cache; +use Utopia\Database\Adapter\Postgres; +use Utopia\Database\Database; + +class PostgresTest extends Base +{ + public static ?Database $database = null; + protected static string $namespace; + + /** + * Return name of adapter + * + * @return string + */ + public static function getAdapterName(): string + { + return "postgres"; + } + + /** + * @reture Adapter + */ + public static function getDatabase(): Database + { + if (!is_null(self::$database)) { + return self::$database; + } + + $dbHost = 'postgres'; + $dbPort = '5432'; + $dbUser = 'root'; + $dbPass = 'password'; + + $pdo = new PDO("pgsql:host={$dbHost};port={$dbPort};", $dbUser, $dbPass, Postgres::getPDOAttributes()); + $redis = new Redis(); + $redis->connect('redis', 6379); + $redis->flushAll(); + $cache = new Cache(new RedisAdapter($redis)); + + $database = new Database(new Postgres($pdo), $cache); + $database->setDatabase('utopiaTests'); + $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + + if ($database->exists('utopiaTests')) { + $database->delete('utopiaTests'); + } + + $database->create(); + + return self::$database = $database; + } +} diff --git a/tests/e2e/Adapter/SQLiteTest.php b/tests/e2e/Adapter/SQLiteTest.php index 16c36f6fd..8451dfa91 100644 --- a/tests/e2e/Adapter/SQLiteTest.php +++ b/tests/e2e/Adapter/SQLiteTest.php @@ -1,65 +1,65 @@ connect('redis'); - $redis->flushAll(); - - $cache = new Cache(new RedisAdapter($redis)); - - $database = new Database(new SQLite($pdo), $cache); - $database->setDatabase('utopiaTests'); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); - - if ($database->exists()) { - $database->delete(); - } - - $database->create(); - - return self::$database = $database; - } -} +// +//namespace Tests\E2E\Adapter; +// +//use PDO; +//use Redis; +//use Utopia\Cache\Adapter\Redis as RedisAdapter; +//use Utopia\Cache\Cache; +//use Utopia\Database\Adapter\SQLite; +//use Utopia\Database\Database; +// +//class SQLiteTest extends Base +//{ +// public static ?Database $database = null; +// protected static string $namespace; +// +// // Remove once all methods are implemented +// /** +// * Return name of adapter +// * +// * @return string +// */ +// public static function getAdapterName(): string +// { +// return "sqlite"; +// } +// +// /** +// * @return Database +// */ +// public static function getDatabase(): Database +// { +// if (!is_null(self::$database)) { +// return self::$database; +// } +// +// $db = __DIR__."/database.sql"; +// +// if (file_exists($db)) { +// unlink($db); +// } +// +// $dsn = $db; +// //$dsn = 'memory'; // Overwrite for fast tests +// $pdo = new PDO("sqlite:" . $dsn, null, null, SQLite::getPDOAttributes()); +// +// $redis = new Redis(); +// $redis->connect('redis'); +// $redis->flushAll(); +// +// $cache = new Cache(new RedisAdapter($redis)); +// +// $database = new Database(new SQLite($pdo), $cache); +// $database->setDatabase('utopiaTests'); +// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); +// +// if ($database->exists()) { +// $database->delete(); +// } +// +// $database->create(); +// +// return self::$database = $database; +// } +//} From 1ce5bdfaf6bc2b563364733cbf7fd1341ecc1a79 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 4 Jan 2024 20:59:48 +0200 Subject: [PATCH 21/52] Change createIndex signature --- src/Database/Adapter.php | 7 +- src/Database/Adapter/MariaDB.php | 99 ++++++++++++++---------- src/Database/Adapter/MySQL.php | 49 ++---------- src/Database/Adapter/Postgres.php | 78 ++++++------------- src/Database/Adapter/SQLite.php | 1 - src/Database/Database.php | 9 +-- tests/e2e/Adapter/MySQLTest.php | 122 +++++++++++++++--------------- 7 files changed, 158 insertions(+), 207 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index beb0ec866..88632e087 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -430,10 +430,13 @@ abstract public function renameIndex(string $collection, string $old, string $ne * @param string $collection * @param string $id * @param string $type - * @param array $attributes + * @param array $attributes + * @param array $lengths + * @param array $orders + * @param array $collectionAttributes * @return bool */ - abstract public function createIndex(string $collection, string $id, string $type, array $attributes): bool; + abstract public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $collectionAttributes): bool; /** * Delete Index diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 197b62fd1..fa9e3aaa1 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -604,27 +604,40 @@ public function renameIndex(string $collection, string $old, string $new): bool * @param string $collection * @param string $id * @param string $type - * @param array $attributes + * @param array $attributes + * @param array $lengths + * @param array $orders + * @param array $collectionAttributes * @return bool * @throws DatabaseException */ - public function createIndex(string $collection, string $id, string $type, array $attributes): bool + public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $collectionAttributes): bool { - $name = $this->filter($collection); + $collection = $this->filter($collection); $id = $this->filter($id); - foreach ($attributes as $key => $attribute) { - $length = empty($attribute['length']) ? '' : '(' . (int)$attribute['length'] . ')'; - $order = empty($attribute['order']) || Database::INDEX_FULLTEXT === $type ? '' : $attribute['order']; + foreach ($attributes as $i => $attr) { + $collectionAttribute = \array_filter($collectionAttributes, function ($collectionAttribute) use ($attr) { + return $collectionAttribute->getAttribute('key') === $attr; + }); + + $collectionAttribute = end($collectionAttribute); - $attr = match ($attribute['attribute']) { + $order = empty($orders[$i]) || Database::INDEX_FULLTEXT === $type ? '' : $orders[$i]; + $length = empty($lengths[$i]) ? '' : '(' . (int)$lengths[$i] . ')'; + + $attr = match ($attr) { '$id' => '_uid', '$createdAt' => '_createdAt', '$updatedAt' => '_updatedAt', - default => $this->filter($attribute['attribute']), + default => $this->filter($attr), }; - $attributes[$key] = "`{$attr}`{$length} {$order}"; + $attributes[$i] = "`{$attr}`{$length} {$order}"; + + if($collectionAttribute !== false && $collectionAttribute->getAttribute('array', false) && $this->castIndexArray()){ + $attributes[$i] = '(CAST(' . $attr . ' AS char(255) ARRAY))'; + } } $sqlType = match ($type) { @@ -641,7 +654,7 @@ public function createIndex(string $collection, string $id, string $type, array $attributes = "_tenant, {$attributes}"; } - $sql = "CREATE {$sqlType} `{$id}` ON {$this->getSQLTable($name)} ({$attributes})"; + $sql = "CREATE {$sqlType} `{$id}` ON {$this->getSQLTable($collection)} ({$attributes})"; var_dump($sql); $sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql); @@ -650,6 +663,14 @@ public function createIndex(string $collection, string $id, string $type, array ->execute(); } + /** + * @return bool + */ + public function castIndexArray(): bool + { + return false; + } + /** * Delete Index * @@ -1997,35 +2018,35 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool } } - /** - * Get SQL Index - * - * @param string $collection - * @param string $id - * @param string $type - * @param array $attributes - * @return string - * @throws Exception - */ - protected function getSQLIndex(string $collection, string $id, string $type, array $attributes): string - { - $sqlType = match ($type) { - Database::INDEX_KEY, - Database::INDEX_ARRAY => 'INDEX', - Database::INDEX_UNIQUE => 'UNIQUE INDEX', - Database::INDEX_FULLTEXT => 'FULLTEXT INDEX', - default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT), - }; - - $attributes = \implode(', ', $attributes); - - if ($this->shareTables && $type !== Database::INDEX_FULLTEXT) { - // Add tenant as first index column for best performance - $attributes = "_tenant, {$attributes}"; - } - - return "CREATE {$sqlType} `{$id}` ON {$this->getSQLTable($collection)} ({$attributes})"; - } +// /** +// * Get SQL Index +// * +// * @param string $collection +// * @param string $id +// * @param string $type +// * @param array $attributes +// * @return string +// * @throws Exception +// */ +// protected function getSQLIndex(string $collection, string $id, string $type, array $attributes): string +// { +// $sqlType = match ($type) { +// Database::INDEX_KEY, +// Database::INDEX_ARRAY => 'INDEX', +// Database::INDEX_UNIQUE => 'UNIQUE INDEX', +// Database::INDEX_FULLTEXT => 'FULLTEXT INDEX', +// default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT), +// }; +// +// $attributes = \implode(', ', $attributes); +// +// if ($this->shareTables && $type !== Database::INDEX_FULLTEXT) { +// // Add tenant as first index column for best performance +// $attributes = "_tenant, {$attributes}"; +// } +// +// return "CREATE {$sqlType} `{$id}` ON {$this->getSQLTable($collection)} ({$attributes})"; +// } /** * Get PDO Type diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 23d98cadc..40e98ec97 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -9,47 +9,6 @@ class MySQL extends MariaDB { - /** - * Get SQL Index - * - * @param string $collection - * @param string $id - * @param string $type - * @param array $attributes - * - * @return string - * @throws DatabaseException - */ - protected function getSQLIndex(string $collection, string $id, string $type, array $attributes): string - { - switch ($type) { - case Database::INDEX_KEY: - $type = 'INDEX'; - break; - - case Database::INDEX_ARRAY: - $type = 'INDEX'; - - foreach ($attributes as $key => $value) { - $attributes[$key] = '(CAST(' . $value . ' AS char(255) ARRAY))'; - } - break; - - case Database::INDEX_UNIQUE: - $type = 'UNIQUE INDEX'; - break; - - case Database::INDEX_FULLTEXT: - $type = 'FULLTEXT INDEX'; - break; - - default: - throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT); - } - - return 'CREATE '.$type.' `'.$id.'` ON `'.$this->getDatabase().'`.`'.$this->getNamespace().'_'.$collection.'` ( '.implode(', ', $attributes).' );'; - } - /** * Set max execution time * @param int $milliseconds @@ -132,4 +91,12 @@ public function getSizeOfCollection(string $collection): int return $size; } + + /** + * @return bool + */ + public function castIndexArray(): bool + { + return true; + } } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 06deba8cd..a5beddfbb 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -76,6 +76,7 @@ public function delete(string $name): bool * @param array $attributes * @param array $indexes * @return bool + * @throws DatabaseException */ public function createCollection(string $name, array $attributes = [], array $indexes = []): bool { @@ -84,7 +85,9 @@ public function createCollection(string $name, array $attributes = [], array $in $this->getPDO()->beginTransaction(); - foreach ($attributes as $index => $attribute) { + /** @var array $attributeStrings */ + $attributeStrings = []; + foreach ($attributes as $attribute) { $attrId = $this->filter($attribute->getId()); $attrType = $this->getSQLType( @@ -94,12 +97,9 @@ public function createCollection(string $name, array $attributes = [], array $in $attribute->getAttribute('array', false) ); - $attributes[$index] = "\"{$attrId}\" {$attrType}, "; + $attributeStrings[] = "\"{$attrId}\" {$attrType}, "; } - /** - * @var array $attributes - */ $sql = " CREATE TABLE IF NOT EXISTS {$this->getSQLTable($id)} ( _id SERIAL NOT NULL, @@ -108,7 +108,7 @@ public function createCollection(string $name, array $attributes = [], array $in \"_createdAt\" TIMESTAMP(3) DEFAULT NULL, \"_updatedAt\" TIMESTAMP(3) DEFAULT NULL, _permissions TEXT DEFAULT NULL, - " . \implode(' ', $attributes) . " + " . \implode(' ', $attributeStrings) . " PRIMARY KEY (_id) ); "; @@ -174,19 +174,14 @@ public function createCollection(string $name, array $attributes = [], array $in $indexOrders = $index->getAttribute('orders', []); $indexAttributes = $index->getAttribute('attributes', []); - foreach ($indexAttributes as $key => $attribute){ - $indexAttributes[$key] = [ - 'attribute' => $attribute, - 'order' => $indexOrders[$key] ?? null, - 'length' => null, - ]; - } - $this->createIndex( $id, $indexId, $indexType, $indexAttributes, + [], + $indexOrders, + $attributes ); } } catch (Exception $e) { @@ -577,29 +572,32 @@ public function deleteRelationship( * @param string $collection * @param string $id * @param string $type - * @param array $attributes + * @param array $attributes + * @param array $lengths + * @param array $orders + * @param array $collectionAttributes * @return bool * @throws DatabaseException */ - public function createIndex(string $collection, string $id, string $type, array $attributes): bool + public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $collectionAttributes): bool { $collection = $this->filter($collection); $id = $this->filter($id); - foreach ($attributes as $key => $attribute) { - $order = empty($attribute['order']) || Database::INDEX_FULLTEXT === $type ? '' : $attribute['order']; + foreach ($attributes as $i => $attr) { + $order = empty($orders[$i]) || Database::INDEX_FULLTEXT === $type ? '' : $orders[$i]; - $attr = match ($attribute['attribute']) { + $attr = match ($attr) { '$id' => '_uid', '$createdAt' => '_createdAt', '$updatedAt' => '_updatedAt', - default => $this->filter($attribute['attribute']), + default => $this->filter($attr), }; if (Database::INDEX_UNIQUE === $type) { - $attributes[$key] = "LOWER(\"{$attr}\"::text) {$order}"; + $attributes[$i] = "LOWER(\"{$attr}\"::text) {$order}"; } else { - $attributes[$key] = "\"{$attr}\" {$order}"; + $attributes[$i] = "\"{$attr}\" {$order}"; } } @@ -1941,8 +1939,10 @@ protected function getFulltextValue(string $value): string * * @param string $type * @param int $size in chars - * + * @param bool $signed + * @param bool $array * @return string + * @throws DatabaseException */ protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false): string { @@ -1984,38 +1984,6 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool } } - /** - * Get SQL Index - * - * @param string $collection - * @param string $id - * @param string $type - * @param array $attributes - * - * @return string - * @throws Exception - */ - protected function getSQLIndex(string $collection, string $id, string $type, array $attributes): string - { - $sqlType = match ($type) { - Database::INDEX_KEY, - Database::INDEX_ARRAY, - Database::INDEX_FULLTEXT => 'INDEX', - Database::INDEX_UNIQUE => 'UNIQUE INDEX', - default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT), - }; - - $key = "\"{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}\""; - $attributes = \implode(', ', $attributes); - - if ($this->shareTables && $type !== Database::INDEX_FULLTEXT) { - // Add tenant as first index column for best performance - $attributes = "_tenant, {$attributes}"; - } - - return "CREATE {$sqlType} {$key} ON {$this->getSQLTable($collection)} ({$attributes});"; - } - /** * Get SQL schema * diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 3da5ea5af..0dfd74d82 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -385,7 +385,6 @@ public function createIndex(string $collection, string $id, string $type, array $name = $this->filter($collection); $id = $this->filter($id); - // Workaround for no support for CREATE INDEX IF NOT EXISTS $stmt = $this->getPDO()->prepare(" SELECT name diff --git a/src/Database/Database.php b/src/Database/Database.php index 722424f64..3a1d1146b 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2393,15 +2393,8 @@ public function createIndex(string $collection, string $id, string $type, array } } - foreach ($attributes as $key => $attribute){ - $attributes[$key] = [ - 'attribute' => $attribute, - 'order' => $orders[$key] ?? null, - 'length' => $lengths[$key] ?? null, - ]; - } + $index = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes, $lengths, $orders, $collection->getAttribute('attributes', [])); - $index = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes); if ($collection->getId() !== self::METADATA) { $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection)); diff --git a/tests/e2e/Adapter/MySQLTest.php b/tests/e2e/Adapter/MySQLTest.php index 11fcbfb02..d204e8a40 100644 --- a/tests/e2e/Adapter/MySQLTest.php +++ b/tests/e2e/Adapter/MySQLTest.php @@ -1,62 +1,62 @@ connect('redis', 6379); -// $redis->flushAll(); -// -// $cache = new Cache(new RedisAdapter($redis)); -// -// $database = new Database(new MySQL($pdo), $cache); -// $database->setDatabase('utopiaTests'); -// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); -// -// if ($database->exists('utopiaTests')) { -// $database->delete('utopiaTests'); -// } -// -// $database->create(); -// -// return self::$database = $database; -// } -//} + +namespace Tests\E2E\Adapter; + +use PDO; +use Redis; +use Utopia\Cache\Adapter\Redis as RedisAdapter; +use Utopia\Cache\Cache; +use Utopia\Database\Adapter\MySQL; +use Utopia\Database\Database; + +class MySQLTest extends Base +{ + public static ?Database $database = null; + protected static string $namespace; + + // Remove once all methods are implemented + /** + * Return name of adapter + * + * @return string + */ + public static function getAdapterName(): string + { + return "mysql"; + } + + /** + * @return Database + */ + public static function getDatabase(): Database + { + if (!is_null(self::$database)) { + return self::$database; + } + + $dbHost = 'mysql'; + $dbPort = '3307'; + $dbUser = 'root'; + $dbPass = 'password'; + + $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MySQL::getPDOAttributes()); + + $redis = new Redis(); + $redis->connect('redis', 6379); + $redis->flushAll(); + + $cache = new Cache(new RedisAdapter($redis)); + + $database = new Database(new MySQL($pdo), $cache); + $database->setDatabase('utopiaTests'); + $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + + if ($database->exists('utopiaTests')) { + $database->delete('utopiaTests'); + } + + $database->create(); + + return self::$database = $database; + } +} From 5f4e2911c88efb735c55d546e865a4ee7c65feb7 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 7 Jan 2024 09:43:22 +0200 Subject: [PATCH 22/52] Mongo --- src/Database/Adapter.php | 7 -- src/Database/Adapter/Mongo.php | 27 ++-- src/Database/Adapter/SQL.php | 13 +- src/Database/Adapter/SQLite.php | 6 +- src/Database/Database.php | 7 +- src/Database/Validator/Index.php | 3 +- tests/e2e/Adapter/Base.php | 14 +-- tests/e2e/Adapter/MariaDBTest.php | 118 +++++++++--------- tests/e2e/Adapter/MongoDBTest.php | 192 ++++++++++++++--------------- tests/e2e/Adapter/MySQLTest.php | 122 +++++++++--------- tests/e2e/Adapter/PostgresTest.php | 116 ++++++++--------- 11 files changed, 295 insertions(+), 330 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 88632e087..6d0077f8f 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -606,13 +606,6 @@ abstract public function getSupportForSchemas(): bool; */ abstract public function getSupportForIndex(): bool; - /** - * Is index array supported? - * - * @return bool - */ - abstract public function getSupportForIndexArray(): bool; - /** * Is unique index supported? * diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 38247e47e..d04c6446c 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -510,13 +510,16 @@ public function deleteRelationship( * @param string $id * @param string $type * @param array $attributes + * @param array $lengths + * @param array $orders + * @param array $collectionAttributes * @param array $collation * @return bool * @throws DatabaseException * @throws MongoException * @throws Exception */ - public function createIndex(string $collection, string $id, string $type, array $attributes, array $collation = []): bool + public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $collectionAttributes, array $collation = []): bool { $name = $this->getNamespace() . '_' . $this->filter($collection); $id = $this->filter($id); @@ -527,14 +530,11 @@ public function createIndex(string $collection, string $id, string $type, array // pass in custom index name $indexes['name'] = $id; - foreach ($attributes as $attribute) { - - $attr = $attribute['attribute']; - $attr = $this->filter($attr); + foreach ($attributes as $i => $attribute) { + $attribute = $this->filter($attribute); - $order = $this->filter($attribute['order']); - $order = empty($order) ? Database::ORDER_ASC : $order; - $indexes['key'][$attr] = $this->getOrder($order); + $orderType = $this->getOrder($this->filter($orders[$i] ?? Database::ORDER_ASC)); + $indexes['key'][$attribute] = $orderType; switch ($type) { case Database::INDEX_KEY: @@ -592,6 +592,7 @@ public function renameIndex(string $collection, string $old, string $new): bool $index['attributes'], $index['lengths'] ?? [], $index['orders'] ?? [], + [] )) { return true; } @@ -1560,16 +1561,6 @@ public function getSupportForIndex(): bool return true; } - /** - * Is index array supported? - * - * @return bool - */ - public function getSupportForIndexArray(): bool - { - return true; - } - /** * Is unique index supported? * diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 007337ac2..a055ed0f2 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -229,16 +229,6 @@ public function getSupportForIndex(): bool return true; } - /** - * Is index supported? - * - * @return bool - */ - public function getSupportForIndexArray(): bool - { - return true; - } - /** * Is unique index supported? * @@ -831,7 +821,6 @@ protected function getSQLIndexType(string $type): string { switch ($type) { case Database::INDEX_KEY: - case Database::INDEX_ARRAY: return 'INDEX'; case Database::INDEX_UNIQUE: @@ -841,7 +830,7 @@ protected function getSQLIndexType(string $type): string return 'FULLTEXT INDEX'; default: - throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT); + throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT); } } diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 0dfd74d82..403b56a30 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -1092,14 +1092,13 @@ protected function getSQLIndexType(string $type): string { switch ($type) { case Database::INDEX_KEY: - case Database::INDEX_ARRAY: return 'INDEX'; case Database::INDEX_UNIQUE: return 'UNIQUE INDEX'; default: - throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT); + throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT); } } @@ -1119,7 +1118,6 @@ protected function getSQLIndex(string $collection, string $id, string $type, arr switch ($type) { case Database::INDEX_KEY: - case Database::INDEX_ARRAY: $type = 'INDEX'; break; @@ -1130,7 +1128,7 @@ protected function getSQLIndex(string $collection, string $id, string $type, arr break; default: - throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT); + throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT); } $attributes = \array_map(fn ($attribute) => match ($attribute) { diff --git a/src/Database/Database.php b/src/Database/Database.php index 3a1d1146b..47255b2bc 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2362,12 +2362,7 @@ public function createIndex(string $collection, string $id, string $type, array throw new DatabaseException('Fulltext index is not supported'); } break; - - case self::INDEX_ARRAY: - if (!$this->adapter->getSupportForIndexArray()) { - throw new DatabaseException('Key index array is not supported'); - } - break; + default: throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT); } diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 2ed0af009..e50d28f34 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -20,7 +20,6 @@ class Index extends Validator /** * @param array $attributes * @param int $maxLength - * @param bool $indexArraySupport * @throws DatabaseException */ public function __construct(array $attributes, int $maxLength) @@ -128,7 +127,7 @@ public function checkArrayIndex(Document $index): bool if($attribute->getAttribute('array') === true){ // Database::INDEX_UNIQUE Is not allowed! since mariaDB VS MySQL makes the unique Different on values - if(!in_array($index->getAttribute('type'), [Database::INDEX_ARRAY, Database::INDEX_KEY])){ + if(!in_array($index->getAttribute('type'), [Database::INDEX_KEY])){ $this->message = ucfirst($index->getAttribute('type')) . '" index is forbidden on array attributes'; return false; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 29d43e27a..57e774208 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1725,11 +1725,13 @@ public function testArrayAttribute(): void $this->assertEquals('Index length for array not specified', $e->getMessage()); } - try { - static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['long_names'], [1000], []); - $this->fail('Failed to throw exception'); - } catch(Throwable $e) { - $this->assertEquals('Index length is longer than the maximum: 768', $e->getMessage()); + if (static::getDatabase()->getAdapter()->getMaxIndexLength() > 0) { + try { + static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['long_names'], [1000], []); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + $this->assertEquals('Index length is longer than the maximum: 768', $e->getMessage()); + } } try { @@ -1766,8 +1768,6 @@ public function testArrayAttribute(): void ]); $this->assertCount(1, $documents); } - - //$this->assertEquals(true,false); } /** diff --git a/tests/e2e/Adapter/MariaDBTest.php b/tests/e2e/Adapter/MariaDBTest.php index e26ca69c2..fc3b8ce77 100644 --- a/tests/e2e/Adapter/MariaDBTest.php +++ b/tests/e2e/Adapter/MariaDBTest.php @@ -1,60 +1,60 @@ connect('redis', 6379); - $redis->flushAll(); - $cache = new Cache(new RedisAdapter($redis)); - - $database = new Database(new MariaDB($pdo), $cache); - $database->setDatabase('utopiaTests'); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); - - if ($database->exists('utopiaTests')) { - $database->delete('utopiaTests'); - } - - $database->create(); - - return self::$database = $database; - } -} +// +//namespace Tests\E2E\Adapter; +// +//use PDO; +//use Redis; +//use Utopia\Cache\Adapter\Redis as RedisAdapter; +//use Utopia\Cache\Cache; +//use Utopia\Database\Adapter\MariaDB; +//use Utopia\Database\Database; +// +//class MariaDBTest extends Base +//{ +// protected static ?Database $database = null; +// protected static string $namespace; +// +// // Remove once all methods are implemented +// /** +// * Return name of adapter +// * +// * @return string +// */ +// public static function getAdapterName(): string +// { +// return "mariadb"; +// } +// +// /** +// * @return Database +// */ +// public static function getDatabase(bool $fresh = false): Database +// { +// if (!is_null(self::$database) && !$fresh) { +// return self::$database; +// } +// +// $dbHost = 'mariadb'; +// $dbPort = '3306'; +// $dbUser = 'root'; +// $dbPass = 'password'; +// +// $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MariaDB::getPDOAttributes()); +// $redis = new Redis(); +// $redis->connect('redis', 6379); +// $redis->flushAll(); +// $cache = new Cache(new RedisAdapter($redis)); +// +// $database = new Database(new MariaDB($pdo), $cache); +// $database->setDatabase('utopiaTests'); +// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); +// +// if ($database->exists('utopiaTests')) { +// $database->delete('utopiaTests'); +// } +// +// $database->create(); +// +// return self::$database = $database; +// } +//} diff --git a/tests/e2e/Adapter/MongoDBTest.php b/tests/e2e/Adapter/MongoDBTest.php index c348bd331..887c26ec2 100644 --- a/tests/e2e/Adapter/MongoDBTest.php +++ b/tests/e2e/Adapter/MongoDBTest.php @@ -1,97 +1,97 @@ connect('redis', 6379); -// $redis->flushAll(); -// $cache = new Cache(new RedisAdapter($redis)); -// -// $schema = 'utopiaTests'; // same as $this->testDatabase -// $client = new Client( -// $schema, -// 'mongo', -// 27017, -// 'root', -// 'example', -// false -// ); -// -// $database = new Database(new Mongo($client), $cache); -// $database->setDatabase($schema); -// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); -// -// if ($database->exists('utopiaTests')) { -// $database->delete('utopiaTests'); -// } -// -// $database->create(); -// -// return self::$database = $database; -// } -// -// /** -// * @throws Exception -// */ -// public function testCreateExistsDelete(): void -// { -// // Mongo creates databases on the fly, so exists would always pass. So we override this test to remove the exists check. -// $this->assertNotNull(static::getDatabase()->create()); -// $this->assertEquals(true, static::getDatabase()->delete($this->testDatabase)); -// $this->assertEquals(true, static::getDatabase()->create()); -// $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase)); -// } -// -// public function testRenameAttribute(): void -// { -// $this->assertTrue(true); -// } -// -// public function testRenameAttributeExisting(): void -// { -// $this->assertTrue(true); -// } -// -// public function testUpdateAttributeStructure(): void -// { -// $this->assertTrue(true); -// } -// -// public function testKeywords(): void -// { -// $this->assertTrue(true); -// } -//} + +namespace Tests\E2E\Adapter; + +use Exception; +use Redis; +use Utopia\Cache\Adapter\Redis as RedisAdapter; +use Utopia\Cache\Cache; +use Utopia\Database\Adapter\Mongo; +use Utopia\Database\Database; +use Utopia\Mongo\Client; + +class MongoDBTest extends Base +{ + public static ?Database $database = null; + protected static string $namespace; + + /** + * Return name of adapter + * + * @return string + */ + public static function getAdapterName(): string + { + return "mongodb"; + } + + /** + * @return Database + * @throws Exception + */ + public static function getDatabase(): Database + { + if (!is_null(self::$database)) { + return self::$database; + } + + $redis = new Redis(); + $redis->connect('redis', 6379); + $redis->flushAll(); + $cache = new Cache(new RedisAdapter($redis)); + + $schema = 'utopiaTests'; // same as $this->testDatabase + $client = new Client( + $schema, + 'mongo', + 27017, + 'root', + 'example', + false + ); + + $database = new Database(new Mongo($client), $cache); + $database->setDatabase($schema); + $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + + if ($database->exists('utopiaTests')) { + $database->delete('utopiaTests'); + } + + $database->create(); + + return self::$database = $database; + } + + /** + * @throws Exception + */ + public function testCreateExistsDelete(): void + { + // Mongo creates databases on the fly, so exists would always pass. So we override this test to remove the exists check. + $this->assertNotNull(static::getDatabase()->create()); + $this->assertEquals(true, static::getDatabase()->delete($this->testDatabase)); + $this->assertEquals(true, static::getDatabase()->create()); + $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase)); + } + + public function testRenameAttribute(): void + { + $this->assertTrue(true); + } + + public function testRenameAttributeExisting(): void + { + $this->assertTrue(true); + } + + public function testUpdateAttributeStructure(): void + { + $this->assertTrue(true); + } + + public function testKeywords(): void + { + $this->assertTrue(true); + } +} diff --git a/tests/e2e/Adapter/MySQLTest.php b/tests/e2e/Adapter/MySQLTest.php index d204e8a40..11fcbfb02 100644 --- a/tests/e2e/Adapter/MySQLTest.php +++ b/tests/e2e/Adapter/MySQLTest.php @@ -1,62 +1,62 @@ connect('redis', 6379); - $redis->flushAll(); - - $cache = new Cache(new RedisAdapter($redis)); - - $database = new Database(new MySQL($pdo), $cache); - $database->setDatabase('utopiaTests'); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); - - if ($database->exists('utopiaTests')) { - $database->delete('utopiaTests'); - } - - $database->create(); - - return self::$database = $database; - } -} +// +//namespace Tests\E2E\Adapter; +// +//use PDO; +//use Redis; +//use Utopia\Cache\Adapter\Redis as RedisAdapter; +//use Utopia\Cache\Cache; +//use Utopia\Database\Adapter\MySQL; +//use Utopia\Database\Database; +// +//class MySQLTest extends Base +//{ +// public static ?Database $database = null; +// protected static string $namespace; +// +// // Remove once all methods are implemented +// /** +// * Return name of adapter +// * +// * @return string +// */ +// public static function getAdapterName(): string +// { +// return "mysql"; +// } +// +// /** +// * @return Database +// */ +// public static function getDatabase(): Database +// { +// if (!is_null(self::$database)) { +// return self::$database; +// } +// +// $dbHost = 'mysql'; +// $dbPort = '3307'; +// $dbUser = 'root'; +// $dbPass = 'password'; +// +// $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MySQL::getPDOAttributes()); +// +// $redis = new Redis(); +// $redis->connect('redis', 6379); +// $redis->flushAll(); +// +// $cache = new Cache(new RedisAdapter($redis)); +// +// $database = new Database(new MySQL($pdo), $cache); +// $database->setDatabase('utopiaTests'); +// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); +// +// if ($database->exists('utopiaTests')) { +// $database->delete('utopiaTests'); +// } +// +// $database->create(); +// +// return self::$database = $database; +// } +//} diff --git a/tests/e2e/Adapter/PostgresTest.php b/tests/e2e/Adapter/PostgresTest.php index 6a12ecd8c..048801724 100644 --- a/tests/e2e/Adapter/PostgresTest.php +++ b/tests/e2e/Adapter/PostgresTest.php @@ -1,59 +1,59 @@ connect('redis', 6379); - $redis->flushAll(); - $cache = new Cache(new RedisAdapter($redis)); - - $database = new Database(new Postgres($pdo), $cache); - $database->setDatabase('utopiaTests'); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); - - if ($database->exists('utopiaTests')) { - $database->delete('utopiaTests'); - } - - $database->create(); - - return self::$database = $database; - } -} +// +//namespace Tests\E2E\Adapter; +// +//use PDO; +//use Redis; +//use Utopia\Cache\Adapter\Redis as RedisAdapter; +//use Utopia\Cache\Cache; +//use Utopia\Database\Adapter\Postgres; +//use Utopia\Database\Database; +// +//class PostgresTest extends Base +//{ +// public static ?Database $database = null; +// protected static string $namespace; +// +// /** +// * Return name of adapter +// * +// * @return string +// */ +// public static function getAdapterName(): string +// { +// return "postgres"; +// } +// +// /** +// * @reture Adapter +// */ +// public static function getDatabase(): Database +// { +// if (!is_null(self::$database)) { +// return self::$database; +// } +// +// $dbHost = 'postgres'; +// $dbPort = '5432'; +// $dbUser = 'root'; +// $dbPass = 'password'; +// +// $pdo = new PDO("pgsql:host={$dbHost};port={$dbPort};", $dbUser, $dbPass, Postgres::getPDOAttributes()); +// $redis = new Redis(); +// $redis->connect('redis', 6379); +// $redis->flushAll(); +// $cache = new Cache(new RedisAdapter($redis)); +// +// $database = new Database(new Postgres($pdo), $cache); +// $database->setDatabase('utopiaTests'); +// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); +// +// if ($database->exists('utopiaTests')) { +// $database->delete('utopiaTests'); +// } +// +// $database->create(); +// +// return self::$database = $database; +// } +//} From 9ba5e822a8413598252425a3eb69ce840d8ef3c2 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 7 Jan 2024 18:38:42 +0200 Subject: [PATCH 23/52] more tests --- src/Database/Adapter/MariaDB.php | 8 +- src/Database/Adapter/SQLite.php | 25 +++--- tests/e2e/Adapter/Base.php | 14 +++- tests/e2e/Adapter/MariaDBTest.php | 118 +++++++++++++------------- tests/e2e/Adapter/MySQLTest.php | 122 +++++++++++++-------------- tests/e2e/Adapter/PostgresTest.php | 116 +++++++++++++------------- tests/e2e/Adapter/SQLiteTest.php | 128 ++++++++++++++--------------- 7 files changed, 274 insertions(+), 257 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index fa9e3aaa1..22583937b 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -617,9 +617,11 @@ public function createIndex(string $collection, string $id, string $type, array $id = $this->filter($id); foreach ($attributes as $i => $attr) { - $collectionAttribute = \array_filter($collectionAttributes, function ($collectionAttribute) use ($attr) { - return $collectionAttribute->getAttribute('key') === $attr; - }); + $collectionAttribute = \array_filter($collectionAttributes, fn ($collectionAttribute) => $collectionAttribute->getAttribute('key') === $attr); + +// $collectionAttribute = \array_filter($collectionAttributes, function ($collectionAttribute) use ($attr) { +// return $collectionAttribute->getAttribute('key') === $attr; +// }); $collectionAttribute = end($collectionAttribute); diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 403b56a30..594fe1a62 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -146,12 +146,12 @@ public function createCollection(string $name, array $attributes = [], array $in $this->getPDO()->prepare($sql)->execute(); - $this->createIndex($id, '_index1', Database::INDEX_UNIQUE, ['_uid'], [], []); - $this->createIndex($id, '_created_at', Database::INDEX_KEY, [ '_createdAt'], [], []); - $this->createIndex($id, '_updated_at', Database::INDEX_KEY, [ '_updatedAt'], [], []); + $this->createIndex($id, '_index1', Database::INDEX_UNIQUE, ['_uid'], [], [], []); + $this->createIndex($id, '_created_at', Database::INDEX_KEY, [ '_createdAt'], [], [], []); + $this->createIndex($id, '_updated_at', Database::INDEX_KEY, [ '_updatedAt'], [], [], []); if ($this->shareTables) { - $this->createIndex($id, '_tenant_id', Database::INDEX_KEY, [ '_id'], [], []); + $this->createIndex($id, '_tenant_id', Database::INDEX_KEY, [ '_id'], [], [], []); } foreach ($indexes as $index) { @@ -161,7 +161,7 @@ public function createCollection(string $name, array $attributes = [], array $in $indexLengths = $index->getAttribute('lengths', []); $indexOrders = $index->getAttribute('orders', []); - $this->createIndex($id, $indexId, $indexType, $indexAttributes, $indexLengths, $indexOrders); + $this->createIndex($id, $indexId, $indexType, $indexAttributes, $indexLengths, $indexOrders, []); } $sql = " @@ -180,8 +180,8 @@ public function createCollection(string $name, array $attributes = [], array $in ->prepare($sql) ->execute(); - $this->createIndex("{$id}_perms", '_index_1', Database::INDEX_UNIQUE, ['_document', '_type', '_permission'], [], []); - $this->createIndex("{$id}_perms", '_index_2', Database::INDEX_KEY, ['_permission', '_type'], [], []); + $this->createIndex("{$id}_perms", '_index_1', Database::INDEX_UNIQUE, ['_document', '_type', '_permission'], [], [], []); + $this->createIndex("{$id}_perms", '_index_2', Database::INDEX_KEY, ['_permission', '_type'], [], [], []); $this->getPDO()->commit(); @@ -312,7 +312,7 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa $this->deleteIndex($name, $index['$id']); } elseif (\in_array($id, $attributes)) { $this->deleteIndex($name, $index['$id']); - $this->createIndex($name, $index['$id'], $index['type'], \array_diff($attributes, [$id]), $index['lengths'], $index['orders']); + $this->createIndex($name, $index['$id'], $index['type'], \array_diff($attributes, [$id]), $index['lengths'], $index['orders'], []); } } @@ -363,6 +363,7 @@ public function renameIndex(string $collection, string $old, string $new): bool $index['attributes'], $index['lengths'], $index['orders'], + [] )) { return true; } @@ -377,10 +378,13 @@ public function renameIndex(string $collection, string $old, string $new): bool * @param string $id * @param string $type * @param array $attributes + * @param array $lengths + * @param array $orders + * @param array $collectionAttributes * @return bool * @throws DatabaseException */ - public function createIndex(string $collection, string $id, string $type, array $attributes): bool + public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $collectionAttributes): bool { $name = $this->filter($collection); $id = $this->filter($id); @@ -389,8 +393,7 @@ public function createIndex(string $collection, string $id, string $type, array $stmt = $this->getPDO()->prepare(" SELECT name FROM sqlite_master - WHERE type='index' - AND name=:_index; + WHERE type='index' AND name=:_index; "); $stmt->bindValue(':_index', "{$this->getNamespace()}_{$this->tenant}_{$name}_{$id}"); $stmt->execute(); diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 57e774208..5eb479877 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1691,7 +1691,12 @@ public function testArrayAttribute(): void static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_FULLTEXT, ['names']); $this->fail('Failed to throw exception'); } catch(Throwable $e) { - $this->assertEquals('Fulltext" index is forbidden on array attributes', $e->getMessage()); + if ($this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { + $this->assertEquals('Fulltext" index is forbidden on array attributes', $e->getMessage()); + } + else { + $this->assertEquals('Fulltext index is not supported', $e->getMessage()); + } } try { @@ -1753,6 +1758,13 @@ public function testArrayAttribute(): void $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_age_names2', Database::INDEX_KEY, ['age', 'booleans'], [0, 255], [])); if ($this->getDatabase()->getAdapter()->getSupportForQueryContains()) { + // todo: show we throw exception using equal on array attribute? + // todo: should contains ['Joe', 'black'] means where 'Joe' and 'black' VS OR? VS JSON_OVERLAPS which means OR + $documents = static::getDatabase()->find($collection, [ + Query::equal('names', ['Joe']) + ]); + $this->assertCount(0, $documents); + $documents = static::getDatabase()->find($collection, [ Query::contains('names', ['Jake', 'Joe']) ]); diff --git a/tests/e2e/Adapter/MariaDBTest.php b/tests/e2e/Adapter/MariaDBTest.php index fc3b8ce77..e26ca69c2 100644 --- a/tests/e2e/Adapter/MariaDBTest.php +++ b/tests/e2e/Adapter/MariaDBTest.php @@ -1,60 +1,60 @@ connect('redis', 6379); -// $redis->flushAll(); -// $cache = new Cache(new RedisAdapter($redis)); -// -// $database = new Database(new MariaDB($pdo), $cache); -// $database->setDatabase('utopiaTests'); -// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); -// -// if ($database->exists('utopiaTests')) { -// $database->delete('utopiaTests'); -// } -// -// $database->create(); -// -// return self::$database = $database; -// } -//} + +namespace Tests\E2E\Adapter; + +use PDO; +use Redis; +use Utopia\Cache\Adapter\Redis as RedisAdapter; +use Utopia\Cache\Cache; +use Utopia\Database\Adapter\MariaDB; +use Utopia\Database\Database; + +class MariaDBTest extends Base +{ + protected static ?Database $database = null; + protected static string $namespace; + + // Remove once all methods are implemented + /** + * Return name of adapter + * + * @return string + */ + public static function getAdapterName(): string + { + return "mariadb"; + } + + /** + * @return Database + */ + public static function getDatabase(bool $fresh = false): Database + { + if (!is_null(self::$database) && !$fresh) { + return self::$database; + } + + $dbHost = 'mariadb'; + $dbPort = '3306'; + $dbUser = 'root'; + $dbPass = 'password'; + + $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MariaDB::getPDOAttributes()); + $redis = new Redis(); + $redis->connect('redis', 6379); + $redis->flushAll(); + $cache = new Cache(new RedisAdapter($redis)); + + $database = new Database(new MariaDB($pdo), $cache); + $database->setDatabase('utopiaTests'); + $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + + if ($database->exists('utopiaTests')) { + $database->delete('utopiaTests'); + } + + $database->create(); + + return self::$database = $database; + } +} diff --git a/tests/e2e/Adapter/MySQLTest.php b/tests/e2e/Adapter/MySQLTest.php index 11fcbfb02..d204e8a40 100644 --- a/tests/e2e/Adapter/MySQLTest.php +++ b/tests/e2e/Adapter/MySQLTest.php @@ -1,62 +1,62 @@ connect('redis', 6379); -// $redis->flushAll(); -// -// $cache = new Cache(new RedisAdapter($redis)); -// -// $database = new Database(new MySQL($pdo), $cache); -// $database->setDatabase('utopiaTests'); -// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); -// -// if ($database->exists('utopiaTests')) { -// $database->delete('utopiaTests'); -// } -// -// $database->create(); -// -// return self::$database = $database; -// } -//} + +namespace Tests\E2E\Adapter; + +use PDO; +use Redis; +use Utopia\Cache\Adapter\Redis as RedisAdapter; +use Utopia\Cache\Cache; +use Utopia\Database\Adapter\MySQL; +use Utopia\Database\Database; + +class MySQLTest extends Base +{ + public static ?Database $database = null; + protected static string $namespace; + + // Remove once all methods are implemented + /** + * Return name of adapter + * + * @return string + */ + public static function getAdapterName(): string + { + return "mysql"; + } + + /** + * @return Database + */ + public static function getDatabase(): Database + { + if (!is_null(self::$database)) { + return self::$database; + } + + $dbHost = 'mysql'; + $dbPort = '3307'; + $dbUser = 'root'; + $dbPass = 'password'; + + $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MySQL::getPDOAttributes()); + + $redis = new Redis(); + $redis->connect('redis', 6379); + $redis->flushAll(); + + $cache = new Cache(new RedisAdapter($redis)); + + $database = new Database(new MySQL($pdo), $cache); + $database->setDatabase('utopiaTests'); + $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + + if ($database->exists('utopiaTests')) { + $database->delete('utopiaTests'); + } + + $database->create(); + + return self::$database = $database; + } +} diff --git a/tests/e2e/Adapter/PostgresTest.php b/tests/e2e/Adapter/PostgresTest.php index 048801724..6a12ecd8c 100644 --- a/tests/e2e/Adapter/PostgresTest.php +++ b/tests/e2e/Adapter/PostgresTest.php @@ -1,59 +1,59 @@ connect('redis', 6379); -// $redis->flushAll(); -// $cache = new Cache(new RedisAdapter($redis)); -// -// $database = new Database(new Postgres($pdo), $cache); -// $database->setDatabase('utopiaTests'); -// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); -// -// if ($database->exists('utopiaTests')) { -// $database->delete('utopiaTests'); -// } -// -// $database->create(); -// -// return self::$database = $database; -// } -//} + +namespace Tests\E2E\Adapter; + +use PDO; +use Redis; +use Utopia\Cache\Adapter\Redis as RedisAdapter; +use Utopia\Cache\Cache; +use Utopia\Database\Adapter\Postgres; +use Utopia\Database\Database; + +class PostgresTest extends Base +{ + public static ?Database $database = null; + protected static string $namespace; + + /** + * Return name of adapter + * + * @return string + */ + public static function getAdapterName(): string + { + return "postgres"; + } + + /** + * @reture Adapter + */ + public static function getDatabase(): Database + { + if (!is_null(self::$database)) { + return self::$database; + } + + $dbHost = 'postgres'; + $dbPort = '5432'; + $dbUser = 'root'; + $dbPass = 'password'; + + $pdo = new PDO("pgsql:host={$dbHost};port={$dbPort};", $dbUser, $dbPass, Postgres::getPDOAttributes()); + $redis = new Redis(); + $redis->connect('redis', 6379); + $redis->flushAll(); + $cache = new Cache(new RedisAdapter($redis)); + + $database = new Database(new Postgres($pdo), $cache); + $database->setDatabase('utopiaTests'); + $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + + if ($database->exists('utopiaTests')) { + $database->delete('utopiaTests'); + } + + $database->create(); + + return self::$database = $database; + } +} diff --git a/tests/e2e/Adapter/SQLiteTest.php b/tests/e2e/Adapter/SQLiteTest.php index 8451dfa91..16c36f6fd 100644 --- a/tests/e2e/Adapter/SQLiteTest.php +++ b/tests/e2e/Adapter/SQLiteTest.php @@ -1,65 +1,65 @@ connect('redis'); -// $redis->flushAll(); -// -// $cache = new Cache(new RedisAdapter($redis)); -// -// $database = new Database(new SQLite($pdo), $cache); -// $database->setDatabase('utopiaTests'); -// $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); -// -// if ($database->exists()) { -// $database->delete(); -// } -// -// $database->create(); -// -// return self::$database = $database; -// } -//} + +namespace Tests\E2E\Adapter; + +use PDO; +use Redis; +use Utopia\Cache\Adapter\Redis as RedisAdapter; +use Utopia\Cache\Cache; +use Utopia\Database\Adapter\SQLite; +use Utopia\Database\Database; + +class SQLiteTest extends Base +{ + public static ?Database $database = null; + protected static string $namespace; + + // Remove once all methods are implemented + /** + * Return name of adapter + * + * @return string + */ + public static function getAdapterName(): string + { + return "sqlite"; + } + + /** + * @return Database + */ + public static function getDatabase(): Database + { + if (!is_null(self::$database)) { + return self::$database; + } + + $db = __DIR__."/database.sql"; + + if (file_exists($db)) { + unlink($db); + } + + $dsn = $db; + //$dsn = 'memory'; // Overwrite for fast tests + $pdo = new PDO("sqlite:" . $dsn, null, null, SQLite::getPDOAttributes()); + + $redis = new Redis(); + $redis->connect('redis'); + $redis->flushAll(); + + $cache = new Cache(new RedisAdapter($redis)); + + $database = new Database(new SQLite($pdo), $cache); + $database->setDatabase('utopiaTests'); + $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + + if ($database->exists()) { + $database->delete(); + } + + $database->create(); + + return self::$database = $database; + } +} From b8ab8779add62dcbe2d60a48711d57295c0b727c Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 7 Jan 2024 18:45:25 +0200 Subject: [PATCH 24/52] more tests --- tests/e2e/Adapter/Base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 5eb479877..d1c12ffed 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1763,7 +1763,7 @@ public function testArrayAttribute(): void $documents = static::getDatabase()->find($collection, [ Query::equal('names', ['Joe']) ]); - $this->assertCount(0, $documents); + //$this->assertCount(0, $documents); $documents = static::getDatabase()->find($collection, [ Query::contains('names', ['Jake', 'Joe']) From 6d6ff1cf5e4f4606180c023d26c8f406227c66bb Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 8 Jan 2024 19:38:55 +0200 Subject: [PATCH 25/52] tests --- src/Database/Adapter.php | 4 +- src/Database/Adapter/MariaDB.php | 86 ++++++++++++------------- src/Database/Adapter/Mongo.php | 12 ++-- src/Database/Adapter/Postgres.php | 14 ++-- src/Database/Adapter/SQL.php | 3 +- src/Database/Adapter/SQLite.php | 43 +++++++------ src/Database/Database.php | 3 +- src/Database/Validator/Index.php | 12 ++-- src/Database/Validator/Query/Filter.php | 32 ++++----- tests/e2e/Adapter/Base.php | 26 ++++++-- 10 files changed, 122 insertions(+), 113 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 6d0077f8f..764f41df6 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -433,10 +433,10 @@ abstract public function renameIndex(string $collection, string $old, string $ne * @param array $attributes * @param array $lengths * @param array $orders - * @param array $collectionAttributes + * * @return bool */ - abstract public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $collectionAttributes): bool; + abstract public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders): bool; /** * Delete Index diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 22583937b..678b876d6 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -607,24 +607,24 @@ public function renameIndex(string $collection, string $old, string $new): bool * @param array $attributes * @param array $lengths * @param array $orders - * @param array $collectionAttributes * @return bool * @throws DatabaseException */ - public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $collectionAttributes): bool + public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders): bool { - $collection = $this->filter($collection); - $id = $this->filter($id); + $collection = $this->getDocument(Database::METADATA, $collection); - foreach ($attributes as $i => $attr) { - $collectionAttribute = \array_filter($collectionAttributes, fn ($collectionAttribute) => $collectionAttribute->getAttribute('key') === $attr); + if ($collection->isEmpty()) { + throw new DatabaseException('Collection not found'); + } -// $collectionAttribute = \array_filter($collectionAttributes, function ($collectionAttribute) use ($attr) { -// return $collectionAttribute->getAttribute('key') === $attr; -// }); + $collectionAttributes = \json_decode($collection->getAttribute('attributes', []), true); - $collectionAttribute = end($collectionAttribute); + $id = $this->filter($id); + foreach ($attributes as $i => $attr) { + $collectionAttribute = \array_filter($collectionAttributes, fn ($collectionAttribute) => array_key_exists('key', $collectionAttribute) && $collectionAttribute['key'] === $attr); + $collectionAttribute = end($collectionAttribute); $order = empty($orders[$i]) || Database::INDEX_FULLTEXT === $type ? '' : $orders[$i]; $length = empty($lengths[$i]) ? '' : '(' . (int)$lengths[$i] . ')'; @@ -637,7 +637,7 @@ public function createIndex(string $collection, string $id, string $type, array $attributes[$i] = "`{$attr}`{$length} {$order}"; - if($collectionAttribute !== false && $collectionAttribute->getAttribute('array', false) && $this->castIndexArray()){ + if(!empty($collectionAttribute['array']) && $collectionAttribute['array'] === true && $this->castIndexArray()) { $attributes[$i] = '(CAST(' . $attr . ' AS char(255) ARRAY))'; } } @@ -656,8 +656,7 @@ public function createIndex(string $collection, string $id, string $type, array $attributes = "_tenant, {$attributes}"; } - $sql = "CREATE {$sqlType} `{$id}` ON {$this->getSQLTable($collection)} ({$attributes})"; - var_dump($sql); + $sql = "CREATE {$sqlType} `{$id}` ON {$this->getSQLTable($collection->getId())} ({$attributes})"; $sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql); return $this->getPDO() @@ -1630,7 +1629,6 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, {$sqlLimit}; "; - var_dump($sql); $sql = $this->trigger(Database::EVENT_DOCUMENT_FIND, $sql); $stmt = $this->getPDO()->prepare($sql); @@ -1972,7 +1970,7 @@ protected function getSQLCondition(Query $query): string */ protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false): string { - if($array === true){ + if($array === true) { return 'JSON'; } @@ -2020,35 +2018,35 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool } } -// /** -// * Get SQL Index -// * -// * @param string $collection -// * @param string $id -// * @param string $type -// * @param array $attributes -// * @return string -// * @throws Exception -// */ -// protected function getSQLIndex(string $collection, string $id, string $type, array $attributes): string -// { -// $sqlType = match ($type) { -// Database::INDEX_KEY, -// Database::INDEX_ARRAY => 'INDEX', -// Database::INDEX_UNIQUE => 'UNIQUE INDEX', -// Database::INDEX_FULLTEXT => 'FULLTEXT INDEX', -// default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT), -// }; -// -// $attributes = \implode(', ', $attributes); -// -// if ($this->shareTables && $type !== Database::INDEX_FULLTEXT) { -// // Add tenant as first index column for best performance -// $attributes = "_tenant, {$attributes}"; -// } -// -// return "CREATE {$sqlType} `{$id}` ON {$this->getSQLTable($collection)} ({$attributes})"; -// } + // /** + // * Get SQL Index + // * + // * @param string $collection + // * @param string $id + // * @param string $type + // * @param array $attributes + // * @return string + // * @throws Exception + // */ + // protected function getSQLIndex(string $collection, string $id, string $type, array $attributes): string + // { + // $sqlType = match ($type) { + // Database::INDEX_KEY, + // Database::INDEX_ARRAY => 'INDEX', + // Database::INDEX_UNIQUE => 'UNIQUE INDEX', + // Database::INDEX_FULLTEXT => 'FULLTEXT INDEX', + // default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT), + // }; + // + // $attributes = \implode(', ', $attributes); + // + // if ($this->shareTables && $type !== Database::INDEX_FULLTEXT) { + // // Add tenant as first index column for best performance + // $attributes = "_tenant, {$attributes}"; + // } + // + // return "CREATE {$sqlType} `{$id}` ON {$this->getSQLTable($collection)} ({$attributes})"; + // } /** * Get PDO Type diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index d04c6446c..bf0971040 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -509,17 +509,14 @@ public function deleteRelationship( * @param string $collection * @param string $id * @param string $type - * @param array $attributes - * @param array $lengths - * @param array $orders - * @param array $collectionAttributes + * @param array $attributes + * @param array $lengths + * @param array $orders * @param array $collation * @return bool - * @throws DatabaseException - * @throws MongoException * @throws Exception */ - public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $collectionAttributes, array $collation = []): bool + public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $collation = []): bool { $name = $this->getNamespace() . '_' . $this->filter($collection); $id = $this->filter($id); @@ -592,7 +589,6 @@ public function renameIndex(string $collection, string $old, string $new): bool $index['attributes'], $index['lengths'] ?? [], $index['orders'] ?? [], - [] )) { return true; } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index a5beddfbb..a26e11523 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -76,7 +76,6 @@ public function delete(string $name): bool * @param array $attributes * @param array $indexes * @return bool - * @throws DatabaseException */ public function createCollection(string $name, array $attributes = [], array $indexes = []): bool { @@ -171,8 +170,8 @@ public function createCollection(string $name, array $attributes = [], array $in foreach ($indexes as $index) { $indexId = $this->filter($index->getId()); $indexType = $index->getAttribute('type'); - $indexOrders = $index->getAttribute('orders', []); $indexAttributes = $index->getAttribute('attributes', []); + $indexOrders = $index->getAttribute('orders', []); $this->createIndex( $id, @@ -180,8 +179,7 @@ public function createCollection(string $name, array $attributes = [], array $in $indexType, $indexAttributes, [], - $indexOrders, - $attributes + $indexOrders ); } } catch (Exception $e) { @@ -575,11 +573,10 @@ public function deleteRelationship( * @param array $attributes * @param array $lengths * @param array $orders - * @param array $collectionAttributes + * * @return bool - * @throws DatabaseException */ - public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $collectionAttributes): bool + public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders): bool { $collection = $this->filter($collection); $id = $this->filter($id); @@ -617,7 +614,6 @@ public function createIndex(string $collection, string $id, string $type, array } $sql = "CREATE {$sqlType} {$key} ON {$this->getSQLTable($collection)} ({$attributes});"; - var_dump($sql); $sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql); return $this->getPDO() @@ -1946,7 +1942,7 @@ protected function getFulltextValue(string $value): string */ protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false): string { - if($array === true){ + if($array === true) { return 'JSONB'; } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index a055ed0f2..d994b17e5 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -865,10 +865,11 @@ protected function getSQLPermissionsCondition(string $collection, array $roles): * * @param string $name * @return string + * @throws DatabaseException */ protected function getSQLTable(string $name): string { - return "`{$this->getDatabase()}`.`{$this->getNamespace()}_{$name}`"; + return "`{$this->getDatabase()}`.`{$this->getNamespace()}_{$this->filter($name)}`"; } /** diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 594fe1a62..206e7be40 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -106,6 +106,7 @@ public function delete(string $name): bool */ public function createCollection(string $name, array $attributes = [], array $indexes = []): bool { + $namespace = $this->getNamespace(); $id = $this->filter($name); try { @@ -146,12 +147,12 @@ public function createCollection(string $name, array $attributes = [], array $in $this->getPDO()->prepare($sql)->execute(); - $this->createIndex($id, '_index1', Database::INDEX_UNIQUE, ['_uid'], [], [], []); - $this->createIndex($id, '_created_at', Database::INDEX_KEY, [ '_createdAt'], [], [], []); - $this->createIndex($id, '_updated_at', Database::INDEX_KEY, [ '_updatedAt'], [], [], []); + $this->createIndex($id, '_index1', Database::INDEX_UNIQUE, ['_uid'], [], []); + $this->createIndex($id, '_created_at', Database::INDEX_KEY, [ '_createdAt'], [], []); + $this->createIndex($id, '_updated_at', Database::INDEX_KEY, [ '_updatedAt'], [], []); if ($this->shareTables) { - $this->createIndex($id, '_tenant_id', Database::INDEX_KEY, [ '_id'], [], [], []); + $this->createIndex($id, '_tenant_id', Database::INDEX_KEY, [ '_id'], [], []); } foreach ($indexes as $index) { @@ -161,7 +162,7 @@ public function createCollection(string $name, array $attributes = [], array $in $indexLengths = $index->getAttribute('lengths', []); $indexOrders = $index->getAttribute('orders', []); - $this->createIndex($id, $indexId, $indexType, $indexAttributes, $indexLengths, $indexOrders, []); + $this->createIndex($id, $indexId, $indexType, $indexAttributes, $indexLengths, $indexOrders); } $sql = " @@ -180,8 +181,8 @@ public function createCollection(string $name, array $attributes = [], array $in ->prepare($sql) ->execute(); - $this->createIndex("{$id}_perms", '_index_1', Database::INDEX_UNIQUE, ['_document', '_type', '_permission'], [], [], []); - $this->createIndex("{$id}_perms", '_index_2', Database::INDEX_KEY, ['_permission', '_type'], [], [], []); + $this->createIndex("{$id}_perms", '_index_1', Database::INDEX_UNIQUE, ['_document', '_type', '_permission'], [], []); + $this->createIndex("{$id}_perms", '_index_2', Database::INDEX_KEY, ['_permission', '_type'], [], []); $this->getPDO()->commit(); @@ -312,7 +313,7 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa $this->deleteIndex($name, $index['$id']); } elseif (\in_array($id, $attributes)) { $this->deleteIndex($name, $index['$id']); - $this->createIndex($name, $index['$id'], $index['type'], \array_diff($attributes, [$id]), $index['lengths'], $index['orders'], []); + $this->createIndex($name, $index['$id'], $index['type'], \array_diff($attributes, [$id]), $index['lengths'], $index['orders']); } } @@ -340,11 +341,15 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa */ public function renameIndex(string $collection, string $old, string $new): bool { - $collection = $this->filter($collection); - $collectionDocument = $this->getDocument(Database::METADATA, $collection); + $collection = $this->getDocument(Database::METADATA, $collection); + + if ($collection->isEmpty()) { + throw new DatabaseException('Collection not found'); + } + $old = $this->filter($old); $new = $this->filter($new); - $indexes = json_decode($collectionDocument['indexes'], true); + $indexes = \json_decode($collection->getAttribute('indexes', []), true); $index = null; foreach ($indexes as $node) { @@ -355,15 +360,14 @@ public function renameIndex(string $collection, string $old, string $new): bool } if ($index - && $this->deleteIndex($collection, $old) + && $this->deleteIndex($collection->getId(), $old) && $this->createIndex( - $collection, + $collection->getId(), $new, $index['type'], $index['attributes'], $index['lengths'], $index['orders'], - [] )) { return true; } @@ -378,13 +382,13 @@ public function renameIndex(string $collection, string $old, string $new): bool * @param string $id * @param string $type * @param array $attributes - * @param array $lengths - * @param array $orders - * @param array $collectionAttributes + * @param array $lengths + * @param array $orders * @return bool - * @throws DatabaseException + * @throws Exception + * @throws PDOException */ - public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $collectionAttributes): bool + public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders): bool { $name = $this->filter($collection); $id = $this->filter($id); @@ -1151,6 +1155,7 @@ protected function getSQLIndex(string $collection, string $id, string $type, arr $attributes = implode(', ', $attributes); if ($this->shareTables) { + // todo: for share tables should we use tenant as part of key? $key = "`{$this->getNamespace()}_{$collection}_{$id}`"; $attributes = "_tenant {$postfix}, {$attributes}"; } diff --git a/src/Database/Database.php b/src/Database/Database.php index 47255b2bc..bc69711e4 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2388,8 +2388,7 @@ public function createIndex(string $collection, string $id, string $type, array } } - $index = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes, $lengths, $orders, $collection->getAttribute('attributes', [])); - + $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)); diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index e50d28f34..2748456bb 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -125,21 +125,21 @@ public function checkArrayIndex(Document $index): bool foreach ($attributes as $key => $attribute) { $attribute = $this->attributes[\strtolower($attribute)] ?? new Document(); - if($attribute->getAttribute('array') === true){ + if($attribute->getAttribute('array') === true) { // Database::INDEX_UNIQUE Is not allowed! since mariaDB VS MySQL makes the unique Different on values - if(!in_array($index->getAttribute('type'), [Database::INDEX_KEY])){ + if(!in_array($index->getAttribute('type'), [Database::INDEX_KEY])) { $this->message = ucfirst($index->getAttribute('type')) . '" index is forbidden on array attributes'; return false; } $arrayAttributes[] = $attribute->getAttribute('key', ''); - if(count($arrayAttributes) > 1){ + if(count($arrayAttributes) > 1) { $this->message = 'Only a single index can be created on array attributes found "' . implode(',', $arrayAttributes) . '"'; return false; } $direction = $orders[$key] ?? ''; - if(!empty($direction)){ + if(!empty($direction)) { $this->message = 'Invalid index order "' . $direction . '" on array attribute "'. $attribute->getAttribute('key', '') .'"'; return false; } @@ -166,12 +166,12 @@ public function checkIndexLength(Document $index): bool $isArray = $attribute->getAttribute('array', false); - if($isArray && empty($lengths[$attributePosition])){ + if($isArray && empty($lengths[$attributePosition])) { $this->message = 'Index length for array not specified'; return false; } - if(!$isArray && $attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])){ + if(!$isArray && $attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { $this->message = 'Key part length are forbidden on "' . $attribute->getAttribute('type') . '" data-type for "' . $attributeName . '"'; return false; } diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index f5b798de2..907348857 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -64,7 +64,7 @@ protected function isValidAttribute(string $attribute): bool * @param array $values * @return bool */ - protected function isValidAttributeAndValues(string $attribute, array $values): bool + protected function isValidAttributeAndValues(string $attribute, array $values, string $method): bool { if (!$this->isValidAttribute($attribute)) { return false; @@ -101,6 +101,18 @@ protected function isValidAttributeAndValues(string $attribute, array $values): } } + if(isset($attributeSchema['array'])){ + if($attributeSchema['array'] === false && Query::TYPE_CONTAINS === $method) { + $this->message = 'Cannot query contains on attribute "' . $attribute . '" because it is not an array.'; + return false; + } + + if($attributeSchema['array'] === true && Query::TYPE_CONTAINS !== $method) { + $this->message = 'Cannot query '. $method .' on attribute "' . $attribute . '" because it is an array. Please use ' . Query::TYPE_CONTAINS; + return false; + } + } + return true; } @@ -143,18 +155,8 @@ public function isValid($value): bool return false; } - if(!$this->isValidAttributeAndValues($attribute, $value->getValues())) { - return false; - } - - if(Query::TYPE_CONTAINS === $method) { - if($this->schema[$attribute]['array'] === false) { - $this->message = 'Cannot query contains on attribute "' . $attribute . '" because it is not an array.'; - return false; - } - } + return $this->isValidAttributeAndValues($attribute, $value->getValues(), $method); - return true; case Query::TYPE_NOT_EQUAL: case Query::TYPE_LESSER: case Query::TYPE_LESSER_EQUAL: @@ -168,7 +170,7 @@ public function isValid($value): bool return false; } - return $this->isValidAttributeAndValues($attribute, $value->getValues()); + return $this->isValidAttributeAndValues($attribute, $value->getValues(), $method); case Query::TYPE_BETWEEN: if (count($value->getValues()) != 2) { @@ -176,11 +178,11 @@ public function isValid($value): bool return false; } - return $this->isValidAttributeAndValues($attribute, $value->getValues()); + return $this->isValidAttributeAndValues($attribute, $value->getValues(), $method); case Query::TYPE_IS_NULL: case Query::TYPE_IS_NOT_NULL: - return $this->isValidAttributeAndValues($attribute, $value->getValues()); + return $this->isValidAttributeAndValues($attribute, $value->getValues(), $method); case Query::TYPE_OR: case Query::TYPE_AND: diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index d1c12ffed..a2a7526f9 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1693,8 +1693,7 @@ public function testArrayAttribute(): void } catch(Throwable $e) { if ($this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { $this->assertEquals('Fulltext" index is forbidden on array attributes', $e->getMessage()); - } - else { + } else { $this->assertEquals('Fulltext index is not supported', $e->getMessage()); } } @@ -1758,12 +1757,25 @@ public function testArrayAttribute(): void $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_age_names2', Database::INDEX_KEY, ['age', 'booleans'], [0, 255], [])); if ($this->getDatabase()->getAdapter()->getSupportForQueryContains()) { - // todo: show we throw exception using equal on array attribute? // todo: should contains ['Joe', 'black'] means where 'Joe' and 'black' VS OR? VS JSON_OVERLAPS which means OR - $documents = static::getDatabase()->find($collection, [ - Query::equal('names', ['Joe']) - ]); - //$this->assertCount(0, $documents); + + try { + static::getDatabase()->find($collection, [ + Query::equal('names', ['Joe']) + ]); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + $this->assertEquals('Invalid query: Cannot query equal on attribute "names" because it is an array. Please use contains', $e->getMessage()); + } + + try { + static::getDatabase()->find($collection, [ + Query::contains('age', [10]) + ]); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + $this->assertEquals('Invalid query: Cannot query contains on attribute "age" because it is not an array.', $e->getMessage()); + } $documents = static::getDatabase()->find($collection, [ Query::contains('names', ['Jake', 'Joe']) From de4ccaaed29b2d582c2c9ed8228624999153aab5 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 8 Jan 2024 19:44:46 +0200 Subject: [PATCH 26/52] lint + composer.lock --- composer.lock | 24 ++++++++++++------------ src/Database/Validator/Query/Filter.php | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/composer.lock b/composer.lock index a37087417..c021e464e 100644 --- a/composer.lock +++ b/composer.lock @@ -269,16 +269,16 @@ }, { "name": "utopia-php/framework", - "version": "0.32.0", + "version": "0.33.0", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "ad6f7e6d6b38cf5bed4e3af9a1394c59d4bb9225" + "reference": "e3ff6b933082d57b48e7c4267bb605c0bf2250fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/ad6f7e6d6b38cf5bed4e3af9a1394c59d4bb9225", - "reference": "ad6f7e6d6b38cf5bed4e3af9a1394c59d4bb9225", + "url": "https://api.github.com/repos/utopia-php/http/zipball/e3ff6b933082d57b48e7c4267bb605c0bf2250fd", + "reference": "e3ff6b933082d57b48e7c4267bb605c0bf2250fd", "shasum": "" }, "require": { @@ -308,9 +308,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.32.0" + "source": "https://github.com/utopia-php/http/tree/0.33.0" }, - "time": "2023-12-26T14:18:36+00:00" + "time": "2024-01-08T13:30:27+00:00" }, { "name": "utopia-php/mongo", @@ -835,16 +835,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.54", + "version": "1.10.55", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "3e25f279dada0adc14ffd7bad09af2e2fc3523bb" + "reference": "9a88f9d18ddf4cf54c922fbeac16c4cb164c5949" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3e25f279dada0adc14ffd7bad09af2e2fc3523bb", - "reference": "3e25f279dada0adc14ffd7bad09af2e2fc3523bb", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9a88f9d18ddf4cf54c922fbeac16c4cb164c5949", + "reference": "9a88f9d18ddf4cf54c922fbeac16c4cb164c5949", "shasum": "" }, "require": { @@ -893,7 +893,7 @@ "type": "tidelift" } ], - "time": "2024-01-05T15:50:47+00:00" + "time": "2024-01-08T12:32:40+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2604,5 +2604,5 @@ "php": ">=8.0" }, "platform-dev": [], - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index 907348857..1de03c915 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -101,7 +101,7 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s } } - if(isset($attributeSchema['array'])){ + if(isset($attributeSchema['array'])) { if($attributeSchema['array'] === false && Query::TYPE_CONTAINS === $method) { $this->message = 'Cannot query contains on attribute "' . $attribute . '" because it is not an array.'; return false; From 1a38083dc9a5cf59916834f3698af0599b5540b0 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 8 Jan 2024 20:20:37 +0200 Subject: [PATCH 27/52] Move getSQLIndex --- src/Database/Adapter/MariaDB.php | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 678b876d6..3819cd523 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -2018,36 +2018,6 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool } } - // /** - // * Get SQL Index - // * - // * @param string $collection - // * @param string $id - // * @param string $type - // * @param array $attributes - // * @return string - // * @throws Exception - // */ - // protected function getSQLIndex(string $collection, string $id, string $type, array $attributes): string - // { - // $sqlType = match ($type) { - // Database::INDEX_KEY, - // Database::INDEX_ARRAY => 'INDEX', - // Database::INDEX_UNIQUE => 'UNIQUE INDEX', - // Database::INDEX_FULLTEXT => 'FULLTEXT INDEX', - // default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT), - // }; - // - // $attributes = \implode(', ', $attributes); - // - // if ($this->shareTables && $type !== Database::INDEX_FULLTEXT) { - // // Add tenant as first index column for best performance - // $attributes = "_tenant, {$attributes}"; - // } - // - // return "CREATE {$sqlType} `{$id}` ON {$this->getSQLTable($collection)} ({$attributes})"; - // } - /** * Get PDO Type * From f9e3f7fc32dc7cad5d3a126c299619cd2033ca3e Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 8 Jan 2024 20:23:23 +0200 Subject: [PATCH 28/52] Check contains with validator --- src/Database/Database.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index bc69711e4..febbd0167 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4571,17 +4571,6 @@ public function find(string $collection, array $queries = []): array foreach ($queries as $index => &$query) { switch ($query->getMethod()) { - // todo: moved to validator.... - // case Query::TYPE_CONTAINS: - // $attribute = $query->getAttribute(); - // foreach ($collection->getAttribute('attributes', []) as $attr) { - // $key = $attr->getAttribute('key', $attr->getAttribute('$id')); - // $array = $attr->getAttribute('array', false); - // if ($key === $attribute && !$array) { - // throw new DatabaseException('Cannot query contains on attribute "' . $attribute . '" because it is not an array.'); - // } - // } - // break; case Query::TYPE_SELECT: $values = $query->getValues(); foreach ($values as $valueIndex => $value) { From 3410db8546c27b8926c8ed0913f98b561ac90c5f Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 9 Jan 2024 15:41:13 +0200 Subject: [PATCH 29/52] version upgrade --- docker-compose.yml | 2 +- src/Database/Adapter/MariaDB.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 518c145f0..10748489f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,7 +38,7 @@ services: POSTGRES_PASSWORD: password mariadb: - image: mariadb:10.9 # Is it ok to upgrade Appwrite to >= 10.9 for JSON_OVERLAPS + image: mariadb:10.11 container_name: utopia-mariadb networks: - database diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 3819cd523..32b9d17cd 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1942,7 +1942,7 @@ protected function getSQLCondition(Query $query): string return "`table_main`.{$attribute} {$this->getSQLOperator($query->getMethod())}"; case Query::TYPE_CONTAINS: - // todo: change to JSON_OVERLAPS when using mariaDB 10.9 + // todo: use JSON_OVERLAPS .. find a solution to postgres... $conditions = []; foreach ($query->getValues() as $key => $value) { $conditions[] = "JSON_CONTAINS(`table_main`.{$attribute}, :{$placeholder}_{$key})"; From 33fa6c8dac6ae1a8229b3dfda45c671dece9ed39 Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Sun, 14 Jan 2024 10:04:46 +0200 Subject: [PATCH 30/52] Update phpunit.xml Co-authored-by: Jake Barnby --- phpunit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index 783265d80..ccdaa969e 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="true"> + stopOnFailure="false"> ./tests/unit From 5a832a193a31aa80d2d75cd572688d211c5f1d33 Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Sun, 14 Jan 2024 10:07:52 +0200 Subject: [PATCH 31/52] Update src/Database/Validator/Index.php Co-authored-by: Jake Barnby --- src/Database/Validator/Index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 2748456bb..0f8ab4617 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -172,7 +172,7 @@ public function checkIndexLength(Document $index): bool } if(!$isArray && $attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { - $this->message = 'Key part length are forbidden on "' . $attribute->getAttribute('type') . '" data-type for "' . $attributeName . '"'; + $this->message = 'Cannot set a length on "'. $attribute->getAttribute('type') . '" attributes'; return false; } From a5d9acfa1ee497df03a98a12b22cce27d75e53e5 Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Sun, 14 Jan 2024 10:39:46 +0200 Subject: [PATCH 32/52] Update src/Database/Validator/Index.php Co-authored-by: Jake Barnby --- src/Database/Validator/Index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 0f8ab4617..b3e619711 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -140,7 +140,7 @@ public function checkArrayIndex(Document $index): bool $direction = $orders[$key] ?? ''; if(!empty($direction)) { - $this->message = 'Invalid index order "' . $direction . '" on array attribute "'. $attribute->getAttribute('key', '') .'"'; + $this->message = 'An array attribute can not have an index order'; return false; } } From 942476764f1691c471086ae86f49dd924ce07359 Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Sun, 14 Jan 2024 10:42:49 +0200 Subject: [PATCH 33/52] Update bin/tasks/load.php Co-authored-by: Jake Barnby --- bin/tasks/load.php | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/tasks/load.php b/bin/tasks/load.php index 5c409da7a..a094292b3 100644 --- a/bin/tasks/load.php +++ b/bin/tasks/load.php @@ -235,7 +235,6 @@ function createSchema(Database $database): void Authorization::setRole(Role::any()->toString()); - $database->createCollection('articles', permissions: [ Permission::create(Role::any()), Permission::read(Role::any()), From 0bb90a4df03fca2374a4a9bcb20d222c6a37c8af Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Sun, 14 Jan 2024 10:48:08 +0200 Subject: [PATCH 34/52] Update src/Database/Validator/Index.php Co-authored-by: Jake Barnby --- src/Database/Validator/Index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index b3e619711..0aca38d94 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -134,7 +134,7 @@ public function checkArrayIndex(Document $index): bool $arrayAttributes[] = $attribute->getAttribute('key', ''); if(count($arrayAttributes) > 1) { - $this->message = 'Only a single index can be created on array attributes found "' . implode(',', $arrayAttributes) . '"'; + $this->message = 'An index may only contain one array attribute'; return false; } From 5431d5acb2952dd09bb9b6b8637b596ba1b5648c Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 14 Jan 2024 11:15:27 +0200 Subject: [PATCH 35/52] Addressing comments --- src/Database/Adapter/MariaDB.php | 21 ++++++---- src/Database/Adapter/Mongo.php | 7 +++- src/Database/Adapter/Postgres.php | 25 +++++++---- src/Database/Adapter/SQL.php | 20 ++++++++- src/Database/Database.php | 6 +++ src/Database/Query.php | 1 + src/Database/Validator/Index.php | 35 ++++++++-------- src/Database/Validator/Query/Filter.php | 17 ++++---- tests/e2e/Adapter/Base.php | 51 ++++++++++++++--------- tests/unit/Validator/Query/FilterTest.php | 50 +++++++++++++++------- 10 files changed, 154 insertions(+), 79 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 32b9d17cd..4de985ed6 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1628,7 +1628,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, {$sqlOrder} {$sqlLimit}; "; - + var_dump($sql); $sql = $this->trigger(Database::EVENT_DOCUMENT_FIND, $sql); $stmt = $this->getPDO()->prepare($sql); @@ -1942,13 +1942,10 @@ protected function getSQLCondition(Query $query): string return "`table_main`.{$attribute} {$this->getSQLOperator($query->getMethod())}"; case Query::TYPE_CONTAINS: - // todo: use JSON_OVERLAPS .. find a solution to postgres... - $conditions = []; - foreach ($query->getValues() as $key => $value) { - $conditions[] = "JSON_CONTAINS(`table_main`.{$attribute}, :{$placeholder}_{$key})"; + if($this->getSupportForQueryOverlaps() && $query->attributeArray) { + return "JSON_OVERLAPS(`table_main`.{$attribute}, :{$placeholder}_0)"; } - $condition = implode(' OR ', $conditions); - return empty($condition) ? '' : '(' . $condition . ')'; + default: $conditions = []; foreach ($query->getValues() as $key => $value) { @@ -2045,6 +2042,16 @@ public function getSupportForFulltextWildcardIndex(): bool return true; } + /** + * Does the adapter handle Query Array Overlaps? + * + * @return bool + */ + public function getSupportForQueryOverlaps(): bool + { + return true; + } + /** * Are timeouts supported? * diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index bf0971040..b4bb340eb 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1388,7 +1388,12 @@ protected function buildFilter(Query $query): array } elseif ($operator == '$ne' && \is_array($value)) { $filter[$attribute]['$nin'] = $value; } elseif ($operator == '$in') { - $filter[$attribute]['$in'] = $query->getValues(); + if($query->getMethod() === Query::TYPE_CONTAINS && $query->attributeArray === false){ + $filter[$attribute]['$regex'] = $this->escapeWildcards($value); + } + else { + $filter[$attribute]['$in'] = $query->getValues(); + } } elseif ($operator == '$search') { $filter['$text'][$operator] = $value; } elseif ($operator === Query::TYPE_BETWEEN) { diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index a26e11523..05f6f161e 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -16,6 +16,8 @@ class Postgres extends SQL { + protected string $like = 'ILIKE'; + /** * Differences between MariaDB and Postgres * @@ -1603,7 +1605,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, "; $sql = $this->trigger(Database::EVENT_DOCUMENT_FIND, $sql); - + var_dump($sql); $stmt = $this->getPDO()->prepare($sql); foreach ($queries as $query) { @@ -1882,6 +1884,7 @@ protected function getSQLCondition(Query $query): string $attribute = "\"{$query->getAttribute()}\""; $placeholder = $this->getSQLPlaceholder($query); + $operator = null; switch ($query->getMethod()) { case Query::TYPE_SEARCH: @@ -1895,17 +1898,13 @@ protected function getSQLCondition(Query $query): string return "table_main.{$attribute} {$this->getSQLOperator($query->getMethod())}"; case Query::TYPE_CONTAINS: - $conditions = []; - foreach ($query->getValues() as $key => $value) { - $conditions[] = "{$attribute} @> :{$placeholder}_{$key}"; - } - $condition = implode(' OR ', $conditions); - return empty($condition) ? '' : '(' . $condition . ')'; + $operator = $query->attributeArray ? '@>' : null; default: $conditions = []; + $operator = $operator ?? $this->getSQLOperator($query->getMethod()); foreach ($query->getValues() as $key => $value) { - $conditions[] = $attribute.' '.$this->getSQLOperator($query->getMethod()).' :'.$placeholder.'_'.$key; + $conditions[] = $attribute.' '.$operator.' :'.$placeholder.'_'.$key; } $condition = implode(' OR ', $conditions); return empty($condition) ? '' : '(' . $condition . ')'; @@ -2082,6 +2081,16 @@ public function getSupportForTimeouts(): bool return true; } + /** + * Does the adapter handle Query Array Overlaps? + * + * @return bool + */ + public function getSupportForQueryOverlaps(): bool + { + return false; + } + /** * Returns Max Execution Time * @param int $milliseconds diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index d994b17e5..8a0617079 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -14,6 +14,7 @@ abstract class SQL extends Adapter { protected mixed $pdo; + protected string $like = 'LIKE'; /** * Constructor. @@ -682,6 +683,13 @@ public function getSupportForQueryContains(): bool return true; } + /** + * Does the adapter handle array Overlaps? + * + * @return bool + */ + abstract public function getSupportForQueryOverlaps(): bool; + public function getSupportForRelationships(): bool { return true; @@ -706,12 +714,19 @@ protected function bindConditionValue(mixed $stmt, Query $query): void return; } + if($this->getSupportForQueryOverlaps() && $query->attributeArray && $query->getMethod() == Query::TYPE_CONTAINS) { + $placeholder = $this->getSQLPlaceholder($query) . '_0'; + var_dump(json_encode($query->getValues())); + $stmt->bindValue($placeholder, json_encode($query->getValues()), PDO::PARAM_STR); + return; + } + foreach ($query->getValues() as $key => $value) { $value = match ($query->getMethod()) { Query::TYPE_STARTS_WITH => $this->escapeWildcards($value) . '%', Query::TYPE_ENDS_WITH => '%' . $this->escapeWildcards($value), Query::TYPE_SEARCH => $this->getFulltextValue($value), - Query::TYPE_CONTAINS => \json_encode($value), + Query::TYPE_CONTAINS => $query->attributeArray ? \json_encode($value) : '%' . $this->escapeWildcards($value) . '%', default => $value }; @@ -777,7 +792,8 @@ protected function getSQLOperator(string $method): string return 'IS NOT NULL'; case Query::TYPE_STARTS_WITH: case Query::TYPE_ENDS_WITH: - return 'LIKE'; + case Query::TYPE_CONTAINS: + return $this->like; default: throw new DatabaseException('Unknown method: ' . $method); } diff --git a/src/Database/Database.php b/src/Database/Database.php index febbd0167..230484a61 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -5167,6 +5167,12 @@ public static function convertQueries(Document $collection, array $queries): arr $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { + foreach ($queries as $query) { + if ($query->getAttribute() === $attribute->getId()) { + $query->attributeArray = $attribute->getAttribute('array', false); + } + } + if ($attribute->getAttribute('type') == Database::VAR_DATETIME) { foreach ($queries as $index => $query) { if ($query->getAttribute() === $attribute->getId()) { diff --git a/src/Database/Query.php b/src/Database/Query.php index 471848e55..3eec0ef4d 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -70,6 +70,7 @@ class Query protected string $method = ''; protected string $attribute = ''; + public bool $attributeArray = false; /** * @var array diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 0aca38d94..f6a3c3165 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -120,27 +120,40 @@ public function checkArrayIndex(Document $index): bool { $attributes = $index->getAttribute('attributes', []); $orders = $index->getAttribute('orders', []); + $lengths = $index->getAttribute('lengths', []); $arrayAttributes = []; - foreach ($attributes as $key => $attribute) { - $attribute = $this->attributes[\strtolower($attribute)] ?? new Document(); + foreach ($attributes as $attributePosition => $attributeName) { + $attribute = $this->attributes[\strtolower($attributeName)] ?? new Document(); + + $isArray = $attribute->getAttribute('array', false); + + if(!$isArray && $attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { + $this->message = 'Key part length are forbidden on "' . $attribute->getAttribute('type') . '" data-type for "' . $attributeName . '"'; + return false; + } - if($attribute->getAttribute('array') === true) { + if($isArray) { // Database::INDEX_UNIQUE Is not allowed! since mariaDB VS MySQL makes the unique Different on values if(!in_array($index->getAttribute('type'), [Database::INDEX_KEY])) { $this->message = ucfirst($index->getAttribute('type')) . '" index is forbidden on array attributes'; return false; } + if(empty($lengths[$attributePosition])) { + $this->message = 'Index length for array not specified'; + return false; + } + $arrayAttributes[] = $attribute->getAttribute('key', ''); if(count($arrayAttributes) > 1) { $this->message = 'An index may only contain one array attribute'; return false; } - $direction = $orders[$key] ?? ''; + $direction = $orders[$attributePosition] ?? ''; if(!empty($direction)) { - $this->message = 'An array attribute can not have an index order'; + $this->message = 'Invalid index order "' . $direction . '" on array attribute "'. $attribute->getAttribute('key', '') .'"'; return false; } } @@ -164,18 +177,6 @@ public function checkIndexLength(Document $index): bool foreach ($index->getAttribute('attributes', []) as $attributePosition => $attributeName) { $attribute = $this->attributes[\strtolower($attributeName)]; - $isArray = $attribute->getAttribute('array', false); - - if($isArray && empty($lengths[$attributePosition])) { - $this->message = 'Index length for array not specified'; - return false; - } - - if(!$isArray && $attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { - $this->message = 'Cannot set a length on "'. $attribute->getAttribute('type') . '" attributes'; - return false; - } - switch ($attribute->getAttribute('type')) { case Database::VAR_STRING: $attributeSize = $attribute->getAttribute('size', 0); diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index 1de03c915..fe1d4c416 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -22,6 +22,7 @@ class Filter extends Base public function __construct(array $attributes = [], int $maxValuesCount = 100) { foreach ($attributes as $attribute) { + $attribute->setAttribute('array', $attribute->getAttribute('array', false)); $this->schema[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); } @@ -101,16 +102,14 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s } } - if(isset($attributeSchema['array'])) { - if($attributeSchema['array'] === false && Query::TYPE_CONTAINS === $method) { - $this->message = 'Cannot query contains on attribute "' . $attribute . '" because it is not an array.'; - return false; - } + if($attributeSchema['array'] === false && $method === Query::TYPE_CONTAINS && $attributeSchema['type'] !== Database::VAR_STRING) { + $this->message = 'Cannot query contains on attribute "' . $attribute . '" because it is not an array.'; + return false; + } - if($attributeSchema['array'] === true && Query::TYPE_CONTAINS !== $method) { - $this->message = 'Cannot query '. $method .' on attribute "' . $attribute . '" because it is an array. Please use ' . Query::TYPE_CONTAINS; - return false; - } + if($attributeSchema['array'] === true && !in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL])) { + $this->message = 'Cannot query '. $method .' on attribute "' . $attribute . '" because it is an array. Please use ' . Query::TYPE_CONTAINS; + return false; } return true; diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index a2a7526f9..eabe4f700 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1646,6 +1646,15 @@ public function testArrayAttribute(): void signed: false )); + $this->assertEquals(true, static::getDatabase()->createAttribute( + $collection, + 'tv_show', + Database::VAR_STRING, + size: 100, + required: false, + signed: false, + )); + try { static::getDatabase()->createDocument($collection, new Document([])); $this->fail('Failed to throw exception'); @@ -1679,6 +1688,8 @@ public function testArrayAttribute(): void 'booleans' => [false], 'names' => ['Joe', 'Antony', '100'], 'numbers' => [0, 100, 1000, -1], + 'age' => 41, + 'tv_show' => 'Everybody Loves Raymond', ])); $document = static::getDatabase()->getDocument($collection, 'joe'); @@ -1699,23 +1710,15 @@ public function testArrayAttribute(): void } try { - static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['numbers', 'names']); - $this->fail('Failed to throw exception'); - } catch(Throwable $e) { - $this->assertEquals('Only a single index can be created on array attributes found "numbers,names"', $e->getMessage()); - } - - try { - static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['numbers', 'names']); + static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['numbers', 'names'], [100,100]); $this->fail('Failed to throw exception'); } catch(Throwable $e) { - $this->assertEquals('Only a single index can be created on array attributes found "numbers,names"', $e->getMessage()); + $this->assertEquals('An index may only contain one array attribute', $e->getMessage()); } - $this->assertEquals(true, static::getDatabase()->createAttribute( $collection, - 'long_names', + 'long_size', Database::VAR_STRING, size: 2000, required: false, @@ -1723,7 +1726,7 @@ public function testArrayAttribute(): void )); try { - static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['long_names'], [], []); + static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['long_size'], [], []); $this->fail('Failed to throw exception'); } catch(Throwable $e) { $this->assertEquals('Index length for array not specified', $e->getMessage()); @@ -1731,7 +1734,7 @@ public function testArrayAttribute(): void if (static::getDatabase()->getAdapter()->getMaxIndexLength() > 0) { try { - static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['long_names'], [1000], []); + static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['long_size'], [1000], []); $this->fail('Failed to throw exception'); } catch(Throwable $e) { $this->assertEquals('Index length is longer than the maximum: 768', $e->getMessage()); @@ -1757,11 +1760,9 @@ public function testArrayAttribute(): void $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_age_names2', Database::INDEX_KEY, ['age', 'booleans'], [0, 255], [])); if ($this->getDatabase()->getAdapter()->getSupportForQueryContains()) { - // todo: should contains ['Joe', 'black'] means where 'Joe' and 'black' VS OR? VS JSON_OVERLAPS which means OR - try { static::getDatabase()->find($collection, [ - Query::equal('names', ['Joe']) + Query::equal('names', ['Joe']), ]); $this->fail('Failed to throw exception'); } catch(Throwable $e) { @@ -1777,18 +1778,28 @@ public function testArrayAttribute(): void $this->assertEquals('Invalid query: Cannot query contains on attribute "age" because it is not an array.', $e->getMessage()); } + $documents = static::getDatabase()->find($collection, [ + Query::isNull('long_size') + ]); + $this->assertCount(1, $documents); + + $documents = static::getDatabase()->find($collection, [ + Query::contains('tv_show', ['Love']) + ]); + $this->assertCount(1, $documents); + $documents = static::getDatabase()->find($collection, [ Query::contains('names', ['Jake', 'Joe']) ]); $this->assertCount(1, $documents); $documents = static::getDatabase()->find($collection, [ - Query::contains('numbers', [-1, 0]) + Query::contains('numbers', [-1, 0, 999]) ]); $this->assertCount(1, $documents); $documents = static::getDatabase()->find($collection, [ - Query::contains('booleans', [false]) + Query::contains('booleans', [false, true]) ]); $this->assertCount(1, $documents); } @@ -2176,11 +2187,11 @@ public function testFindContains(): void try { static::getDatabase()->find('movies', [ - Query::contains('name', ['Frozen']), + Query::contains('price', [10.5]), ]); $this->fail('Failed to throw exception'); } catch(Throwable $e) { - $this->assertEquals('Invalid query: Cannot query contains on attribute "name" because it is not an array.', $e->getMessage()); + $this->assertEquals('Invalid query: Cannot query contains on attribute "price" because it is not an array.', $e->getMessage()); $this->assertTrue($e instanceof DatabaseException); } } diff --git a/tests/unit/Validator/Query/FilterTest.php b/tests/unit/Validator/Query/FilterTest.php index c375033fd..01d38b98a 100644 --- a/tests/unit/Validator/Query/FilterTest.php +++ b/tests/unit/Validator/Query/FilterTest.php @@ -13,34 +13,52 @@ class FilterTest extends TestCase { protected Base|null $validator = null; + /** + * @throws \Utopia\Database\Exception + */ public function setUp(): void { $this->validator = new Filter( attributes: [ new Document([ - '$id' => 'attr', - 'key' => 'attr', + '$id' => 'string', + 'key' => 'string', 'type' => Database::VAR_STRING, 'array' => false, ]), new Document([ - '$id' => 'attr_array', - 'key' => 'attr_array', + '$id' => 'string_array', + 'key' => 'string_array', 'type' => Database::VAR_STRING, 'array' => true, ]), + new Document([ + '$id' => 'integer_array', + 'key' => 'integer_array', + 'type' => Database::VAR_INTEGER, + 'array' => true, + ]), + new Document([ + '$id' => 'integer', + 'key' => 'integer', + 'type' => Database::VAR_INTEGER, + 'array' => false, + ]), ], ); } public function testSuccess(): void { - $this->assertTrue($this->validator->isValid(Query::between('attr', '1975-12-06', '2050-12-06'))); - $this->assertTrue($this->validator->isValid(Query::isNotNull('attr'))); - $this->assertTrue($this->validator->isValid(Query::isNull('attr'))); - $this->assertTrue($this->validator->isValid(Query::startsWith('attr', 'super'))); - $this->assertTrue($this->validator->isValid(Query::endsWith('attr', 'man'))); - $this->assertTrue($this->validator->isValid(Query::contains('attr_array', ['super']))); + $this->assertTrue($this->validator->isValid(Query::between('string', '1975-12-06', '2050-12-06'))); + $this->assertTrue($this->validator->isValid(Query::isNotNull('string'))); + $this->assertTrue($this->validator->isValid(Query::isNull('string'))); + $this->assertTrue($this->validator->isValid(Query::startsWith('string', 'super'))); + $this->assertTrue($this->validator->isValid(Query::endsWith('string', 'man'))); + $this->assertTrue($this->validator->isValid(Query::contains('string_array', ['super']))); + $this->assertTrue($this->validator->isValid(Query::contains('integer_array', [100,10,-1]))); + $this->assertTrue($this->validator->isValid(Query::contains('string_array', ["1","10","-1"]))); + $this->assertTrue($this->validator->isValid(Query::contains('string', ['super']))); } public function testFailure(): void @@ -59,19 +77,21 @@ public function testFailure(): void $this->assertFalse($this->validator->isValid(Query::offset(5001))); $this->assertFalse($this->validator->isValid(Query::equal('dne', ['v']))); $this->assertFalse($this->validator->isValid(Query::equal('', ['v']))); - $this->assertFalse($this->validator->isValid(Query::orderAsc('attr'))); - $this->assertFalse($this->validator->isValid(Query::orderDesc('attr'))); + $this->assertFalse($this->validator->isValid(Query::orderAsc('string'))); + $this->assertFalse($this->validator->isValid(Query::orderDesc('string'))); $this->assertFalse($this->validator->isValid(new Query(Query::TYPE_CURSOR_AFTER, values: ['asdf']))); $this->assertFalse($this->validator->isValid(new Query(Query::TYPE_CURSOR_BEFORE, values: ['asdf']))); - $this->assertFalse($this->validator->isValid(Query::contains('attr', ['super']))); + $this->assertFalse($this->validator->isValid(Query::contains('integer', ['super']))); + $this->assertFalse($this->validator->isValid(Query::equal('integer_array', [100,-1]))); + $this->assertFalse($this->validator->isValid(Query::contains('integer_array', [10.6]))); } public function testEmptyValues(): void { - $this->assertFalse($this->validator->isValid(Query::contains('attr', []))); + $this->assertFalse($this->validator->isValid(Query::contains('string', []))); $this->assertEquals('Contains queries require at least one value.', $this->validator->getDescription()); - $this->assertFalse($this->validator->isValid(Query::equal('attr', []))); + $this->assertFalse($this->validator->isValid(Query::equal('string', []))); $this->assertEquals('Equal queries require at least one value.', $this->validator->getDescription()); } } From bb018fc108be3393ef1e91c8d3b8ce77caf1d921 Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Sun, 14 Jan 2024 11:17:02 +0200 Subject: [PATCH 36/52] Update src/Database/Database.php Co-authored-by: Jake Barnby --- src/Database/Database.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 230484a61..d6e668b89 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4560,7 +4560,6 @@ public function find(string $collection, array $queries = []): array $cursor = empty($cursor) ? [] : $this->encode($collection, $cursor)->getArrayCopy(); /** @var array $queries */ - $queries = \array_merge( $selects, self::convertQueries($collection, $filters) From 6a32656892ca28c661c927c8bea95dc29035da7b Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Sun, 14 Jan 2024 11:48:58 +0200 Subject: [PATCH 37/52] Update src/Database/Adapter/SQLite.php Co-authored-by: Jake Barnby --- src/Database/Adapter/SQLite.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 206e7be40..cb67f407b 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -1155,7 +1155,6 @@ protected function getSQLIndex(string $collection, string $id, string $type, arr $attributes = implode(', ', $attributes); if ($this->shareTables) { - // todo: for share tables should we use tenant as part of key? $key = "`{$this->getNamespace()}_{$collection}_{$id}`"; $attributes = "_tenant {$postfix}, {$attributes}"; } From 16df875d920c9d8204e8e0b3ce8c17723c349688 Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Sun, 14 Jan 2024 11:51:16 +0200 Subject: [PATCH 38/52] Update src/Database/Validator/Index.php Co-authored-by: Jake Barnby --- src/Database/Validator/Index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index f6a3c3165..7da8426b9 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -136,7 +136,7 @@ public function checkArrayIndex(Document $index): bool if($isArray) { // Database::INDEX_UNIQUE Is not allowed! since mariaDB VS MySQL makes the unique Different on values if(!in_array($index->getAttribute('type'), [Database::INDEX_KEY])) { - $this->message = ucfirst($index->getAttribute('type')) . '" index is forbidden on array attributes'; + $this->message = '"' . ucfirst($index->getAttribute('type')) . '" index is forbidden on array attributes'; return false; } From 8f13042d91f862015b5d08758f59b1982ca47e69 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 14 Jan 2024 11:52:55 +0200 Subject: [PATCH 39/52] Mysql check cast --- src/Database/Adapter/MariaDB.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 4de985ed6..9e75185a2 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -637,7 +637,7 @@ public function createIndex(string $collection, string $id, string $type, array $attributes[$i] = "`{$attr}`{$length} {$order}"; - if(!empty($collectionAttribute['array']) && $collectionAttribute['array'] === true && $this->castIndexArray()) { + if(!empty($collectionAttribute['array']) && $this->castIndexArray()) { $attributes[$i] = '(CAST(' . $attr . ' AS char(255) ARRAY))'; } } From b0c390005222a60ddc9cdd826eac23fac5d0a841 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 14 Jan 2024 11:58:22 +0200 Subject: [PATCH 40/52] formatting --- src/Database/Adapter/MariaDB.php | 1 + src/Database/Adapter/Mongo.php | 5 ++--- src/Database/Adapter/Postgres.php | 1 + src/Database/Validator/Index.php | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 9e75185a2..ef31aae6c 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1946,6 +1946,7 @@ protected function getSQLCondition(Query $query): string return "JSON_OVERLAPS(`table_main`.{$attribute}, :{$placeholder}_0)"; } + // no break default: $conditions = []; foreach ($query->getValues() as $key => $value) { diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index b4bb340eb..fef5ce01f 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1388,10 +1388,9 @@ protected function buildFilter(Query $query): array } elseif ($operator == '$ne' && \is_array($value)) { $filter[$attribute]['$nin'] = $value; } elseif ($operator == '$in') { - if($query->getMethod() === Query::TYPE_CONTAINS && $query->attributeArray === false){ + if($query->getMethod() === Query::TYPE_CONTAINS && $query->attributeArray === false) { $filter[$attribute]['$regex'] = $this->escapeWildcards($value); - } - else { + } else { $filter[$attribute]['$in'] = $query->getValues(); } } elseif ($operator == '$search') { diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 05f6f161e..793900869 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1900,6 +1900,7 @@ protected function getSQLCondition(Query $query): string case Query::TYPE_CONTAINS: $operator = $query->attributeArray ? '@>' : null; + // no break default: $conditions = []; $operator = $operator ?? $this->getSQLOperator($query->getMethod()); diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 7da8426b9..09ba2effe 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -147,7 +147,7 @@ public function checkArrayIndex(Document $index): bool $arrayAttributes[] = $attribute->getAttribute('key', ''); if(count($arrayAttributes) > 1) { - $this->message = 'An index may only contain one array attribute'; + $this->message = 'An index may only contain one array attribute'; return false; } From 8c8fe43fb21d5ee76b15cedb2531f1ff8fddddc2 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 14 Jan 2024 12:17:42 +0200 Subject: [PATCH 41/52] Cannot set a length text change --- src/Database/Validator/Index.php | 2 +- tests/e2e/Adapter/Base.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 09ba2effe..3f122fca7 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -129,7 +129,7 @@ public function checkArrayIndex(Document $index): bool $isArray = $attribute->getAttribute('array', false); if(!$isArray && $attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { - $this->message = 'Key part length are forbidden on "' . $attribute->getAttribute('type') . '" data-type for "' . $attributeName . '"'; + $this->message = 'Cannot set a length on "'. $attribute->getAttribute('type') . '" attributes'; return false; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index eabe4f700..dc475e193 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1703,7 +1703,7 @@ public function testArrayAttribute(): void $this->fail('Failed to throw exception'); } catch(Throwable $e) { if ($this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { - $this->assertEquals('Fulltext" index is forbidden on array attributes', $e->getMessage()); + $this->assertEquals('"Fulltext" index is forbidden on array attributes', $e->getMessage()); } else { $this->assertEquals('Fulltext index is not supported', $e->getMessage()); } @@ -1752,7 +1752,7 @@ public function testArrayAttribute(): void static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['age', 'names'], [10, 255], []); $this->fail('Failed to throw exception'); } catch(Throwable $e) { - $this->assertEquals('Key part length are forbidden on "integer" data-type for "age"', $e->getMessage()); + $this->assertEquals('Cannot set a length on "integer" attributes', $e->getMessage()); } $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_names', Database::INDEX_KEY, ['names'], [255], [])); From 5a04626d46a051f554a0e7b84e0b5a40b008de1a Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 14 Jan 2024 12:24:16 +0200 Subject: [PATCH 42/52] remove var_dumps --- src/Database/Adapter/MariaDB.php | 3 ++- src/Database/Adapter/Postgres.php | 2 +- src/Database/Adapter/SQL.php | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index ef31aae6c..16aec33a1 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1628,8 +1628,9 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, {$sqlOrder} {$sqlLimit}; "; - var_dump($sql); + $sql = $this->trigger(Database::EVENT_DOCUMENT_FIND, $sql); + $stmt = $this->getPDO()->prepare($sql); foreach ($queries as $query) { diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 793900869..9d254c2d8 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1605,7 +1605,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, "; $sql = $this->trigger(Database::EVENT_DOCUMENT_FIND, $sql); - var_dump($sql); + $stmt = $this->getPDO()->prepare($sql); foreach ($queries as $query) { diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 8a0617079..f42ec5739 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -716,7 +716,6 @@ protected function bindConditionValue(mixed $stmt, Query $query): void if($this->getSupportForQueryOverlaps() && $query->attributeArray && $query->getMethod() == Query::TYPE_CONTAINS) { $placeholder = $this->getSQLPlaceholder($query) . '_0'; - var_dump(json_encode($query->getValues())); $stmt->bindValue($placeholder, json_encode($query->getValues()), PDO::PARAM_STR); return; } From 5dd561bb0dc30ebd2edcf84384c65fe7aa1b1d40 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 15 Jan 2024 10:45:35 +0200 Subject: [PATCH 43/52] Test Integer array size --- src/Database/Validator/Index.php | 15 ++++++--------- tests/e2e/Adapter/Base.php | 9 ++++++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 3f122fca7..1f8aecf25 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -126,16 +126,9 @@ public function checkArrayIndex(Document $index): bool foreach ($attributes as $attributePosition => $attributeName) { $attribute = $this->attributes[\strtolower($attributeName)] ?? new Document(); - $isArray = $attribute->getAttribute('array', false); - - if(!$isArray && $attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { - $this->message = 'Cannot set a length on "'. $attribute->getAttribute('type') . '" attributes'; - return false; - } - - if($isArray) { + if($attribute->getAttribute('array', false)) { // Database::INDEX_UNIQUE Is not allowed! since mariaDB VS MySQL makes the unique Different on values - if(!in_array($index->getAttribute('type'), [Database::INDEX_KEY])) { + if($index->getAttribute('type') != Database::INDEX_KEY) { $this->message = '"' . ucfirst($index->getAttribute('type')) . '" index is forbidden on array attributes'; return false; } @@ -157,6 +150,10 @@ public function checkArrayIndex(Document $index): bool return false; } } + else if($attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { + $this->message = 'Cannot set a length on "'. $attribute->getAttribute('type') . '" attributes'; + return false; + } } return true; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index dc475e193..06b8904e9 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1650,7 +1650,7 @@ public function testArrayAttribute(): void $collection, 'tv_show', Database::VAR_STRING, - size: 100, + size: 700, required: false, signed: false, )); @@ -1683,7 +1683,7 @@ public function testArrayAttribute(): void } static::getDatabase()->createDocument($collection, new Document([ - '$id' => 'joe', + '$id' => 'id1', '$permissions' => $permissions, 'booleans' => [false], 'names' => ['Joe', 'Antony', '100'], @@ -1692,7 +1692,7 @@ public function testArrayAttribute(): void 'tv_show' => 'Everybody Loves Raymond', ])); - $document = static::getDatabase()->getDocument($collection, 'joe'); + $document = static::getDatabase()->getDocument($collection, 'id1'); $this->assertEquals(false, $document->getAttribute('booleans')[0]); $this->assertEquals('Antony', $document->getAttribute('names')[1]); @@ -1725,6 +1725,9 @@ public function testArrayAttribute(): void array: true )); + // tv_show Index length (700) + numbers(?) > 768 should break + $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_numbers', Database::INDEX_KEY, ['tv_show', 'numbers'], [], [])); + try { static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['long_size'], [], []); $this->fail('Failed to throw exception'); From e49ac277604501c387e141464c3e97765eb73b8f Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 16 Jan 2024 10:57:52 +0200 Subject: [PATCH 44/52] Default index length --- src/Database/Adapter/MariaDB.php | 2 +- src/Database/Database.php | 24 +++++++++++++++++++++--- src/Database/Validator/Index.php | 8 ++++++-- tests/e2e/Adapter/Base.php | 31 ++++++++++--------------------- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 16aec33a1..ced5a000c 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -638,7 +638,7 @@ public function createIndex(string $collection, string $id, string $type, array $attributes[$i] = "`{$attr}`{$length} {$order}"; if(!empty($collectionAttribute['array']) && $this->castIndexArray()) { - $attributes[$i] = '(CAST(' . $attr . ' AS char(255) ARRAY))'; + $attributes[$i] = '(CAST(' . $attr . ' AS char(' . Database::ARRAY_INDEX_LENGTH . ') ARRAY))'; } } diff --git a/src/Database/Database.php b/src/Database/Database.php index d6e668b89..0818450c7 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -32,7 +32,7 @@ class Database public const VAR_BOOLEAN = 'boolean'; public const VAR_DATETIME = 'datetime'; - // Relationships Types + // Relationship Types public const VAR_RELATIONSHIP = 'relationship'; // Index Types @@ -40,7 +40,7 @@ class Database public const INDEX_FULLTEXT = 'fulltext'; public const INDEX_UNIQUE = 'unique'; public const INDEX_SPATIAL = 'spatial'; - public const INDEX_ARRAY = 'array'; + public const ARRAY_INDEX_LENGTH = 255; // Relation Types public const RELATION_ONE_TO_ONE = 'oneToOne'; @@ -2364,7 +2364,25 @@ public function createIndex(string $collection, string $id, string $type, array break; default: - throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT); + throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT); + } + + /** @var array $collectionAttributes */ + $collectionAttributes = $collection->getAttribute('attributes', []); + + foreach ($attributes as $i => $attr) { + foreach ($collectionAttributes as $collectionAttribute) { + if($collectionAttribute->getAttribute('key') === $attr) { + $isArray = $collectionAttribute->getAttribute('array', false); + if($isArray) { + if($this->adapter->getMaxIndexLength() > 0) { + $lengths[$i] = self::ARRAY_INDEX_LENGTH; + } + $orders[$i] = null; + } + break; + } + } } $index = new Document([ diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 1f8aecf25..62a03b6f2 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -149,8 +149,7 @@ public function checkArrayIndex(Document $index): bool $this->message = 'Invalid index order "' . $direction . '" on array attribute "'. $attribute->getAttribute('key', '') .'"'; return false; } - } - else if($attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { + } elseif($attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { $this->message = 'Cannot set a length on "'. $attribute->getAttribute('type') . '" attributes'; return false; } @@ -189,6 +188,11 @@ public function checkIndexLength(Document $index): bool break; } + if($attribute->getAttribute('array', false)) { + $attributeSize = Database::ARRAY_INDEX_LENGTH; + $indexLength = Database::ARRAY_INDEX_LENGTH; + } + if ($indexLength > $attributeSize) { $this->message = 'Index length ' . $indexLength . ' is larger than the size for ' . $attributeName . ': ' . $attributeSize . '"'; return false; diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 06b8904e9..c36a17ad3 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1725,42 +1725,31 @@ public function testArrayAttribute(): void array: true )); - // tv_show Index length (700) + numbers(?) > 768 should break - $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_numbers', Database::INDEX_KEY, ['tv_show', 'numbers'], [], [])); - - try { - static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['long_size'], [], []); - $this->fail('Failed to throw exception'); - } catch(Throwable $e) { - $this->assertEquals('Index length for array not specified', $e->getMessage()); - } - if (static::getDatabase()->getAdapter()->getMaxIndexLength() > 0) { + // If getMaxIndexLength() > 0 We clear length for array attributes + static::getDatabase()->createIndex($collection, 'indx1', Database::INDEX_KEY, ['long_size'], [], []); + static::getDatabase()->createIndex($collection, 'indx2', Database::INDEX_KEY, ['long_size'], [1000], []); + try { - static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['long_size'], [1000], []); + static::getDatabase()->createIndex($collection, 'indx_numbers', Database::INDEX_KEY, ['tv_show', 'numbers'], [], []); // [700, 255] $this->fail('Failed to throw exception'); } catch(Throwable $e) { $this->assertEquals('Index length is longer than the maximum: 768', $e->getMessage()); } } - try { - static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['names'], [255], ['desc']); - $this->fail('Failed to throw exception'); - } catch(Throwable $e) { - $this->assertEquals('Invalid index order "desc" on array attribute "names"', $e->getMessage()); - } + // We clear orders for array attributes + static::getDatabase()->createIndex($collection, 'indx3', Database::INDEX_KEY, ['names'], [255], ['desc']); try { - static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['age', 'names'], [10, 255], []); + static::getDatabase()->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()); } - $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_names', Database::INDEX_KEY, ['names'], [255], [])); - $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_age_names1', Database::INDEX_KEY, ['age', 'names'], [null, 255], [])); - $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx_age_names2', Database::INDEX_KEY, ['age', 'booleans'], [0, 255], [])); + $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx6', Database::INDEX_KEY, ['age', 'names'], [null, 999], [])); + $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx7', Database::INDEX_KEY, ['age', 'booleans'], [0, 999], [])); if ($this->getDatabase()->getAdapter()->getSupportForQueryContains()) { try { From 2315c492f1a04f19dfc0b75dc5247d1fd2711f84 Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Wed, 17 Jan 2024 09:50:21 +0200 Subject: [PATCH 45/52] Update src/Database/Adapter/MariaDB.php Co-authored-by: Jake Barnby --- src/Database/Adapter/MariaDB.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index ced5a000c..300f10eab 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -2049,7 +2049,7 @@ public function getSupportForFulltextWildcardIndex(): bool * * @return bool */ - public function getSupportForQueryOverlaps(): bool + public function getSupportForJSONOverlaps(): bool { return true; } From b4869c50005a9f642d8f12587b8bb1dcc2bb3775 Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Wed, 17 Jan 2024 09:51:16 +0200 Subject: [PATCH 46/52] Update src/Database/Adapter/SQL.php Co-authored-by: Jake Barnby --- src/Database/Adapter/SQL.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index f42ec5739..a1aea1268 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -688,7 +688,7 @@ public function getSupportForQueryContains(): bool * * @return bool */ - abstract public function getSupportForQueryOverlaps(): bool; + abstract public function getSupportForJSONOverlaps(): bool; public function getSupportForRelationships(): bool { From 1b999c5c20e20e96f652d0b8e22d8327fee76cb2 Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Wed, 17 Jan 2024 10:15:40 +0200 Subject: [PATCH 47/52] Update src/Database/Validator/Query/Filter.php Co-authored-by: Jake Barnby --- src/Database/Validator/Query/Filter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index fe1d4c416..19f95f7c3 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -108,7 +108,7 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s } if($attributeSchema['array'] === true && !in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL])) { - $this->message = 'Cannot query '. $method .' on attribute "' . $attribute . '" because it is an array. Please use ' . Query::TYPE_CONTAINS; + $this->message = 'Cannot query '. $method .' on attribute "' . $attribute . '" because it is an array.'; return false; } From 35c6d2c79d5fa64266506939c729c5bc9c502804 Mon Sep 17 00:00:00 2001 From: Shmuel Fogel Date: Wed, 17 Jan 2024 10:16:35 +0200 Subject: [PATCH 48/52] Update src/Database/Validator/Query/Filter.php Co-authored-by: Jake Barnby --- src/Database/Validator/Query/Filter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index 19f95f7c3..54b19ba74 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -103,7 +103,7 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s } if($attributeSchema['array'] === false && $method === Query::TYPE_CONTAINS && $attributeSchema['type'] !== Database::VAR_STRING) { - $this->message = 'Cannot query contains on attribute "' . $attribute . '" because it is not an array.'; + $this->message = 'Cannot query contains on attribute "' . $attribute . '" because it is not an array or string.'; return false; } From 2e11ad0da3dc99f37474cdffe3a03350fbb640f5 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 17 Jan 2024 14:52:30 +0200 Subject: [PATCH 49/52] Addressing comments --- src/Database/Adapter/MariaDB.php | 2 +- src/Database/Adapter/Mongo.php | 2 +- src/Database/Adapter/Postgres.php | 14 ++-- src/Database/Adapter/SQL.php | 16 +++-- src/Database/Database.php | 2 +- src/Database/Query.php | 19 +++++- src/Database/Validator/Query/Filter.php | 14 +++- tests/e2e/Adapter/Base.php | 91 ++++++++++++++++--------- 8 files changed, 114 insertions(+), 46 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 300f10eab..475665cd2 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1943,7 +1943,7 @@ protected function getSQLCondition(Query $query): string return "`table_main`.{$attribute} {$this->getSQLOperator($query->getMethod())}"; case Query::TYPE_CONTAINS: - if($this->getSupportForQueryOverlaps() && $query->attributeArray) { + if($this->getSupportForJSONOverlaps() && $query->onArray()) { return "JSON_OVERLAPS(`table_main`.{$attribute}, :{$placeholder}_0)"; } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index fef5ce01f..72a58220e 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1388,7 +1388,7 @@ protected function buildFilter(Query $query): array } elseif ($operator == '$ne' && \is_array($value)) { $filter[$attribute]['$nin'] = $value; } elseif ($operator == '$in') { - if($query->getMethod() === Query::TYPE_CONTAINS && $query->attributeArray === false) { + if($query->getMethod() === Query::TYPE_CONTAINS && !$query->onArray()) { $filter[$attribute]['$regex'] = $this->escapeWildcards($value); } else { $filter[$attribute]['$in'] = $query->getValues(); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 9d254c2d8..be6e6f239 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -16,8 +16,6 @@ class Postgres extends SQL { - protected string $like = 'ILIKE'; - /** * Differences between MariaDB and Postgres * @@ -1898,7 +1896,7 @@ protected function getSQLCondition(Query $query): string return "table_main.{$attribute} {$this->getSQLOperator($query->getMethod())}"; case Query::TYPE_CONTAINS: - $operator = $query->attributeArray ? '@>' : null; + $operator = $query->onArray() ? '@>' : null; // no break default: @@ -2087,7 +2085,7 @@ public function getSupportForTimeouts(): bool * * @return bool */ - public function getSupportForQueryOverlaps(): bool + public function getSupportForJSONOverlaps(): bool { return false; } @@ -2134,4 +2132,12 @@ protected function processException(PDOException $e): void throw $e; } + + /** + * @return string + */ + public function getLikeOperator(): string + { + return 'LIKE'; + } } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index a1aea1268..00b6d5832 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -14,7 +14,6 @@ abstract class SQL extends Adapter { protected mixed $pdo; - protected string $like = 'LIKE'; /** * Constructor. @@ -714,7 +713,7 @@ protected function bindConditionValue(mixed $stmt, Query $query): void return; } - if($this->getSupportForQueryOverlaps() && $query->attributeArray && $query->getMethod() == Query::TYPE_CONTAINS) { + if($this->getSupportForJSONOverlaps() && $query->onArray() && $query->getMethod() == Query::TYPE_CONTAINS) { $placeholder = $this->getSQLPlaceholder($query) . '_0'; $stmt->bindValue($placeholder, json_encode($query->getValues()), PDO::PARAM_STR); return; @@ -725,7 +724,7 @@ protected function bindConditionValue(mixed $stmt, Query $query): void Query::TYPE_STARTS_WITH => $this->escapeWildcards($value) . '%', Query::TYPE_ENDS_WITH => '%' . $this->escapeWildcards($value), Query::TYPE_SEARCH => $this->getFulltextValue($value), - Query::TYPE_CONTAINS => $query->attributeArray ? \json_encode($value) : '%' . $this->escapeWildcards($value) . '%', + Query::TYPE_CONTAINS => $query->onArray() ? \json_encode($value) : '%' . $this->escapeWildcards($value) . '%', default => $value }; @@ -792,7 +791,7 @@ protected function getSQLOperator(string $method): string case Query::TYPE_STARTS_WITH: case Query::TYPE_ENDS_WITH: case Query::TYPE_CONTAINS: - return $this->like; + return $this->getLikeOperator(); default: throw new DatabaseException('Unknown method: ' . $method); } @@ -970,4 +969,13 @@ public function getSQLConditions(array $queries = [], string $separator = 'AND') $tmp = implode(' '. $separator .' ', $conditions); return empty($tmp) ? '' : '(' . $tmp . ')'; } + + /** + * @return string + */ + public function getLikeOperator(): string + { + return 'LIKE'; + } + } diff --git a/src/Database/Database.php b/src/Database/Database.php index 0818450c7..818ebb1b5 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -5186,7 +5186,7 @@ public static function convertQueries(Document $collection, array $queries): arr foreach ($attributes as $attribute) { foreach ($queries as $query) { if ($query->getAttribute() === $attribute->getId()) { - $query->attributeArray = $attribute->getAttribute('array', false); + $query->setOnArray($attribute->getAttribute('array', false)); } } diff --git a/src/Database/Query.php b/src/Database/Query.php index 3eec0ef4d..8c99dec10 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -70,7 +70,7 @@ class Query protected string $method = ''; protected string $attribute = ''; - public bool $attributeArray = false; + protected bool $onArray = false; /** * @var array @@ -679,4 +679,21 @@ public function isNested(): bool return false; } + + /** + * @return bool + */ + public function onArray(): bool + { + return $this->onArray; + } + + /** + * @param bool $bool + * @return void + */ + public function setOnArray(bool $bool): void + { + $this->onArray = $bool; + } } diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index fe1d4c416..e99e2b0c0 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -22,7 +22,6 @@ class Filter extends Base public function __construct(array $attributes = [], int $maxValuesCount = 100) { foreach ($attributes as $attribute) { - $attribute->setAttribute('array', $attribute->getAttribute('array', false)); $this->schema[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); } @@ -102,12 +101,21 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s } } - if($attributeSchema['array'] === false && $method === Query::TYPE_CONTAINS && $attributeSchema['type'] !== Database::VAR_STRING) { + $array = $attributeSchema['array'] ?? false; + + if( + !$array && + $method === Query::TYPE_CONTAINS && + $attributeSchema['type'] !== Database::VAR_STRING + ) { $this->message = 'Cannot query contains on attribute "' . $attribute . '" because it is not an array.'; return false; } - if($attributeSchema['array'] === true && !in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL])) { + if( + $array && + !in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL]) + ) { $this->message = 'Cannot query '. $method .' on attribute "' . $attribute . '" because it is an array. Please use ' . Query::TYPE_CONTAINS; return false; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index c36a17ad3..b40126c73 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1602,14 +1602,15 @@ public function testArrayAttribute(): void { Authorization::setRole(Role::any()->toString()); + $database = static::getDatabase(); $collection = 'json'; $permissions = [Permission::read(Role::any())]; - static::getDatabase()->createCollection($collection, permissions: [ + $database->createCollection($collection, permissions: [ Permission::create(Role::any()), ]); - $this->assertEquals(true, static::getDatabase()->createAttribute( + $this->assertEquals(true, $database->createAttribute( $collection, 'booleans', Database::VAR_BOOLEAN, @@ -1618,7 +1619,7 @@ public function testArrayAttribute(): void array: true )); - $this->assertEquals(true, static::getDatabase()->createAttribute( + $this->assertEquals(true, $database->createAttribute( $collection, 'names', Database::VAR_STRING, @@ -1627,7 +1628,7 @@ public function testArrayAttribute(): void array: true )); - $this->assertEquals(true, static::getDatabase()->createAttribute( + $this->assertEquals(true, $database->createAttribute( $collection, 'numbers', Database::VAR_INTEGER, @@ -1637,7 +1638,7 @@ public function testArrayAttribute(): void array: true )); - $this->assertEquals(true, static::getDatabase()->createAttribute( + $this->assertEquals(true, $database->createAttribute( $collection, 'age', Database::VAR_INTEGER, @@ -1646,7 +1647,7 @@ public function testArrayAttribute(): void signed: false )); - $this->assertEquals(true, static::getDatabase()->createAttribute( + $this->assertEquals(true, $database->createAttribute( $collection, 'tv_show', Database::VAR_STRING, @@ -1655,16 +1656,45 @@ public function testArrayAttribute(): void signed: false, )); + $this->assertEquals(true, $database->createAttribute( + $collection, + 'short', + Database::VAR_STRING, + size: 5, + required: false, + signed: false, + array: true + )); + try { - static::getDatabase()->createDocument($collection, new Document([])); + $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()); } + $database->updateAttribute($collection, 'booleans', required: false); + + $doc = $database->getCollection($collection); + $attribute = $doc->getAttribute('attributes')[0]; + $this->assertEquals('boolean', $attribute['type']); + $this->assertEquals(true, $attribute['signed']); + $this->assertEquals(0, $attribute['size']); + $this->assertEquals(null, $attribute['default']); + $this->assertEquals(true, $attribute['array']); + $this->assertEquals(false, $attribute['required']); + + try { + $database->createDocument($collection, new Document([ + 'short' => ['More than 5 size'], + ])); + $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()); + } + try { - static::getDatabase()->createDocument($collection, new Document([ - 'booleans' => [false], + $database->createDocument($collection, new Document([ 'names' => ['Joe', 100], ])); $this->fail('Failed to throw exception'); @@ -1673,8 +1703,7 @@ public function testArrayAttribute(): void } try { - static::getDatabase()->createDocument($collection, new Document([ - 'booleans' => [false], + $database->createDocument($collection, new Document([ 'age' => 1.5, ])); $this->fail('Failed to throw exception'); @@ -1682,7 +1711,7 @@ public function testArrayAttribute(): void $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid integer', $e->getMessage()); } - static::getDatabase()->createDocument($collection, new Document([ + $database->createDocument($collection, new Document([ '$id' => 'id1', '$permissions' => $permissions, 'booleans' => [false], @@ -1692,14 +1721,14 @@ public function testArrayAttribute(): void 'tv_show' => 'Everybody Loves Raymond', ])); - $document = static::getDatabase()->getDocument($collection, 'id1'); + $document = $database->getDocument($collection, 'id1'); $this->assertEquals(false, $document->getAttribute('booleans')[0]); $this->assertEquals('Antony', $document->getAttribute('names')[1]); $this->assertEquals(100, $document->getAttribute('numbers')[1]); try { - static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_FULLTEXT, ['names']); + $database->createIndex($collection, 'indx', Database::INDEX_FULLTEXT, ['names']); $this->fail('Failed to throw exception'); } catch(Throwable $e) { if ($this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { @@ -1710,13 +1739,13 @@ public function testArrayAttribute(): void } try { - static::getDatabase()->createIndex($collection, 'indx', Database::INDEX_KEY, ['numbers', 'names'], [100,100]); + $database->createIndex($collection, 'indx', Database::INDEX_KEY, ['numbers', 'names'], [100,100]); $this->fail('Failed to throw exception'); } catch(Throwable $e) { $this->assertEquals('An index may only contain one array attribute', $e->getMessage()); } - $this->assertEquals(true, static::getDatabase()->createAttribute( + $this->assertEquals(true, $database->createAttribute( $collection, 'long_size', Database::VAR_STRING, @@ -1725,13 +1754,13 @@ public function testArrayAttribute(): void array: true )); - if (static::getDatabase()->getAdapter()->getMaxIndexLength() > 0) { + if ($database->getAdapter()->getMaxIndexLength() > 0) { // If getMaxIndexLength() > 0 We clear length for array attributes - static::getDatabase()->createIndex($collection, 'indx1', Database::INDEX_KEY, ['long_size'], [], []); - static::getDatabase()->createIndex($collection, 'indx2', Database::INDEX_KEY, ['long_size'], [1000], []); + $database->createIndex($collection, 'indx1', Database::INDEX_KEY, ['long_size'], [], []); + $database->createIndex($collection, 'indx2', Database::INDEX_KEY, ['long_size'], [1000], []); try { - static::getDatabase()->createIndex($collection, 'indx_numbers', Database::INDEX_KEY, ['tv_show', 'numbers'], [], []); // [700, 255] + $database->createIndex($collection, 'indx_numbers', Database::INDEX_KEY, ['tv_show', 'numbers'], [], []); // [700, 255] $this->fail('Failed to throw exception'); } catch(Throwable $e) { $this->assertEquals('Index length is longer than the maximum: 768', $e->getMessage()); @@ -1739,21 +1768,21 @@ public function testArrayAttribute(): void } // We clear orders for array attributes - static::getDatabase()->createIndex($collection, 'indx3', Database::INDEX_KEY, ['names'], [255], ['desc']); + $database->createIndex($collection, 'indx3', Database::INDEX_KEY, ['names'], [255], ['desc']); try { - static::getDatabase()->createIndex($collection, 'indx4', Database::INDEX_KEY, ['age', 'names'], [10, 255], []); + $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()); } - $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx6', Database::INDEX_KEY, ['age', 'names'], [null, 999], [])); - $this->assertTrue(static::getDatabase()->createIndex($collection, 'indx7', Database::INDEX_KEY, ['age', 'booleans'], [0, 999], [])); + $this->assertTrue($database->createIndex($collection, 'indx6', Database::INDEX_KEY, ['age', 'names'], [null, 999], [])); + $this->assertTrue($database->createIndex($collection, 'indx7', Database::INDEX_KEY, ['age', 'booleans'], [0, 999], [])); if ($this->getDatabase()->getAdapter()->getSupportForQueryContains()) { try { - static::getDatabase()->find($collection, [ + $database->find($collection, [ Query::equal('names', ['Joe']), ]); $this->fail('Failed to throw exception'); @@ -1762,7 +1791,7 @@ public function testArrayAttribute(): void } try { - static::getDatabase()->find($collection, [ + $database->find($collection, [ Query::contains('age', [10]) ]); $this->fail('Failed to throw exception'); @@ -1770,27 +1799,27 @@ public function testArrayAttribute(): void $this->assertEquals('Invalid query: Cannot query contains on attribute "age" because it is not an array.', $e->getMessage()); } - $documents = static::getDatabase()->find($collection, [ + $documents = $database->find($collection, [ Query::isNull('long_size') ]); $this->assertCount(1, $documents); - $documents = static::getDatabase()->find($collection, [ + $documents = $database->find($collection, [ Query::contains('tv_show', ['Love']) ]); $this->assertCount(1, $documents); - $documents = static::getDatabase()->find($collection, [ + $documents = $database->find($collection, [ Query::contains('names', ['Jake', 'Joe']) ]); $this->assertCount(1, $documents); - $documents = static::getDatabase()->find($collection, [ + $documents = $database->find($collection, [ Query::contains('numbers', [-1, 0, 999]) ]); $this->assertCount(1, $documents); - $documents = static::getDatabase()->find($collection, [ + $documents = $database->find($collection, [ Query::contains('booleans', [false, true]) ]); $this->assertCount(1, $documents); From 18ba0050ac1a55a3cd660e79529434e7a38a5bfc Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 17 Jan 2024 15:38:03 +0200 Subject: [PATCH 50/52] Addressing comments --- tests/e2e/Adapter/Base.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index b40126c73..ec34ed346 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1787,7 +1787,7 @@ public function testArrayAttribute(): void ]); $this->fail('Failed to throw exception'); } catch(Throwable $e) { - $this->assertEquals('Invalid query: Cannot query equal on attribute "names" because it is an array. Please use contains', $e->getMessage()); + $this->assertEquals('Invalid query: Cannot query equal on attribute "names" because it is an array.', $e->getMessage()); } try { @@ -1796,7 +1796,7 @@ public function testArrayAttribute(): void ]); $this->fail('Failed to throw exception'); } catch(Throwable $e) { - $this->assertEquals('Invalid query: Cannot query contains on attribute "age" because it is not an array.', $e->getMessage()); + $this->assertEquals('Invalid query: Cannot query contains on attribute "age" because it is not an array or string.', $e->getMessage()); } $documents = $database->find($collection, [ @@ -2212,7 +2212,7 @@ public function testFindContains(): void ]); $this->fail('Failed to throw exception'); } catch(Throwable $e) { - $this->assertEquals('Invalid query: Cannot query contains on attribute "price" because it is not an array.', $e->getMessage()); + $this->assertEquals('Invalid query: Cannot query contains on attribute "price" because it is not an array or string.', $e->getMessage()); $this->assertTrue($e instanceof DatabaseException); } } From db7bf054194f3fdfde1d92812110082dbeaf91f3 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 18 Jan 2024 13:41:30 +0200 Subject: [PATCH 51/52] Upper case like --- composer.lock | 42 +++++++++++++++++++------------------- tests/e2e/Adapter/Base.php | 2 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/composer.lock b/composer.lock index c021e464e..122a03843 100644 --- a/composer.lock +++ b/composer.lock @@ -269,16 +269,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.0", + "version": "0.33.1", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "e3ff6b933082d57b48e7c4267bb605c0bf2250fd" + "reference": "b745607aa1875554a0ad52e28f6db918da1ce11c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/e3ff6b933082d57b48e7c4267bb605c0bf2250fd", - "reference": "e3ff6b933082d57b48e7c4267bb605c0bf2250fd", + "url": "https://api.github.com/repos/utopia-php/http/zipball/b745607aa1875554a0ad52e28f6db918da1ce11c", + "reference": "b745607aa1875554a0ad52e28f6db918da1ce11c", "shasum": "" }, "require": { @@ -308,9 +308,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.0" + "source": "https://github.com/utopia-php/http/tree/0.33.1" }, - "time": "2024-01-08T13:30:27+00:00" + "time": "2024-01-17T16:48:32+00:00" }, { "name": "utopia-php/mongo", @@ -509,16 +509,16 @@ }, { "name": "laravel/pint", - "version": "v1.13.7", + "version": "v1.13.9", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "4157768980dbd977f1c4b4cc94997416d8b30ece" + "reference": "e3e269cc5d874c8efd2dc7962b1c7ff2585fe525" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/4157768980dbd977f1c4b4cc94997416d8b30ece", - "reference": "4157768980dbd977f1c4b4cc94997416d8b30ece", + "url": "https://api.github.com/repos/laravel/pint/zipball/e3e269cc5d874c8efd2dc7962b1c7ff2585fe525", + "reference": "e3e269cc5d874c8efd2dc7962b1c7ff2585fe525", "shasum": "" }, "require": { @@ -529,13 +529,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.38.0", - "illuminate/view": "^10.30.1", + "friendsofphp/php-cs-fixer": "^3.47.0", + "illuminate/view": "^10.40.0", + "larastan/larastan": "^2.8.1", "laravel-zero/framework": "^10.3.0", - "mockery/mockery": "^1.6.6", - "nunomaduro/larastan": "^2.6.4", + "mockery/mockery": "^1.6.7", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.24.2" + "pestphp/pest": "^2.31.0" }, "bin": [ "builds/pint" @@ -571,7 +571,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2023-12-05T19:43:12+00:00" + "time": "2024-01-16T17:39:29+00:00" }, { "name": "myclabs/deep-copy", @@ -835,16 +835,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.55", + "version": "1.10.56", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "9a88f9d18ddf4cf54c922fbeac16c4cb164c5949" + "reference": "27816a01aea996191ee14d010f325434c0ee76fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9a88f9d18ddf4cf54c922fbeac16c4cb164c5949", - "reference": "9a88f9d18ddf4cf54c922fbeac16c4cb164c5949", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/27816a01aea996191ee14d010f325434c0ee76fa", + "reference": "27816a01aea996191ee14d010f325434c0ee76fa", "shasum": "" }, "require": { @@ -893,7 +893,7 @@ "type": "tidelift" } ], - "time": "2024-01-08T12:32:40+00:00" + "time": "2024-01-15T10:43:00+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index ec34ed346..7d47a0f40 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1805,7 +1805,7 @@ public function testArrayAttribute(): void $this->assertCount(1, $documents); $documents = $database->find($collection, [ - Query::contains('tv_show', ['Love']) + Query::contains('tv_show', ['love']) ]); $this->assertCount(1, $documents); From 75b75b09f8a849c2fe374472019a9ba5824bad1e Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 18 Jan 2024 14:37:43 +0200 Subject: [PATCH 52/52] Fix Mongo case sensitive --- src/Database/Adapter/Mongo.php | 2 +- src/Database/Adapter/Postgres.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 72a58220e..12cd7a9fa 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1389,7 +1389,7 @@ protected function buildFilter(Query $query): array $filter[$attribute]['$nin'] = $value; } elseif ($operator == '$in') { if($query->getMethod() === Query::TYPE_CONTAINS && !$query->onArray()) { - $filter[$attribute]['$regex'] = $this->escapeWildcards($value); + $filter[$attribute]['$regex'] = new Regex(".*{$this->escapeWildcards($value)}.*", 'i'); } else { $filter[$attribute]['$in'] = $query->getValues(); } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index be6e6f239..149271128 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2138,6 +2138,6 @@ protected function processException(PDOException $e): void */ public function getLikeOperator(): string { - return 'LIKE'; + return 'ILIKE'; } }