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/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->setDatabase($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 4658eadd6..5c409da7a 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 2fcd29143..4ece99e89 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/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 diff --git a/composer.lock b/composer.lock index e12850141..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", @@ -901,23 +901,23 @@ }, { "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", @@ -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", 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/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.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 0efa96df8..3a893be49 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,11 +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); - - if ($array) { - $type = 'LONGTEXT'; - } + $type = $this->getSQLType($type, $size, $signed, $array); $sql = "ALTER TABLE {$this->getSQLTable($name)} ADD COLUMN `{$id}` {$type};"; $sql = $this->trigger(Database::EVENT_ATTRIBUTE_CREATE, $sql); @@ -299,11 +297,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 = 'LONGTEXT'; - } + $type = $this->getSQLType($type, $size, $signed, $array); $sql = "ALTER TABLE {$this->getSQLTable($name)} MODIFY `{$id}` {$type};"; @@ -643,7 +637,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() @@ -1608,8 +1602,8 @@ 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); foreach ($queries as $query) { @@ -1912,19 +1906,27 @@ protected function getSQLCondition(Query $query): string return empty($conditions) ? '' : ' '. $method .' (' . implode(' AND ', $conditions) . ')'; 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: + // 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 . ')'; default: $conditions = []; foreach ($query->getValues() as $key => $value) { - $conditions[] = $attribute . ' ' . $this->getSQLOperator($query->getMethod()) . ' :' . $placeholder . '_' . $key; + $conditions[] = "{$attribute} {$this->getSQLOperator($query->getMethod())} :{$placeholder}_{$key}"; } return empty($conditions) ? '' : '(' . implode(' OR ', $conditions) . ')'; } @@ -1936,11 +1938,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/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/Postgres.php b/src/Database/Adapter/Postgres.php index e52be9b53..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 = 'TEXT'; - } + $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 = 'LONGTEXT'; - } + $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')"; @@ -1879,7 +1873,7 @@ protected function getSQLCondition(Query $query): string default => $query->getAttribute() }); - $attribute = "\"{$query->getAttribute()}\"" ; + $attribute = "\"{$query->getAttribute()}\""; $placeholder = $this->getSQLPlaceholder($query); switch ($query->getMethod()) { @@ -1893,6 +1887,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) { @@ -1929,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/SQL.php b/src/Database/Adapter/SQL.php index 5eab3f3b4..007337ac2 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? * @@ -679,7 +689,7 @@ public function getSupportForCasting(): bool */ public function getSupportForQueryContains(): bool { - return false; + return true; } public function getSupportForRelationships(): bool @@ -711,10 +721,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)); } } @@ -943,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/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 5e9cffd61..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}, "; } @@ -1049,6 +1045,11 @@ public function getSupportForSchemas(): bool return false; } + public function getSupportForQueryContains(): bool + { + return false; + } + /** * Is fulltext index supported? * diff --git a/src/Database/Database.php b/src/Database/Database.php index ca7920ac1..22d5659d1 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2362,7 +2362,12 @@ 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); } @@ -4559,6 +4564,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,6 +4576,17 @@ 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) { diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 6cdf9d89c..f43a2b6c7 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,43 @@ 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(); + 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 = ucfirst($index->getAttribute('type')) . '" index is forbidden on array attributes'; + return false; + } + + $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,6 +166,18 @@ 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 for "' . $attributeName . '"'; + return false; + } + switch ($attribute->getAttribute('type')) { case Database::VAR_STRING: $attributeSize = $attribute->getAttribute('size', 0); @@ -186,6 +235,10 @@ public function isValid($value): bool return false; } + if (!$this->checkArrayIndex($value)) { + return false; + } + if (!$this->checkIndexLength($value)) { return false; } 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 6eb5f4f37..dd7d47843 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1591,23 +1591,211 @@ public function testDeleteDocument(Document $document): void } + /** + * @throws AuthorizationException + * @throws DuplicateException + * @throws ConflictException + * @throws LimitException + * @throws StructureException + */ + public function testArrayAttribute(): void + { + 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, + 'booleans', + Database::VAR_BOOLEAN, + size: 0, + required: true, + array: true + )); + + $this->assertEquals(true, static::getDatabase()->createAttribute( + $collection, + 'names', + Database::VAR_STRING, + size: 255, // Does this mean each Element max is 255? We need to check this on Structure validation? + required: false, + array: true + )); + + $this->assertEquals(true, static::getDatabase()->createAttribute( + $collection, + 'numbers', + Database::VAR_INTEGER, + size: 0, + required: false, + signed: false, + 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'); + } catch(Throwable $e) { + $this->assertEquals('Invalid document structure: Missing required attribute "booleans"', $e->getMessage()); + } + + try { + static::getDatabase()->createDocument($collection, new Document([ + 'booleans' => [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 255 chars', $e->getMessage()); + } + + try { + static::getDatabase()->createDocument($collection, new Document([ + 'booleans' => [false], + 'age' => 1.5, + ])); + $this->fail('Failed to throw exception'); + } catch(Throwable $e) { + $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid integer', $e->getMessage()); + } + + static::getDatabase()->createDocument($collection, new Document([ + '$id' => 'joe', + '$permissions' => $permissions, + 'booleans' => [false], + 'names' => ['Joe', 'Antony', '100'], + 'numbers' => [0, 100, 1000, -1], + ])); + + $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 { + 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()); + } + + 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 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, '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']) + ]); + $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); + } + /** * @return array */ 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)); @@ -1632,7 +1820,7 @@ public function testFind(): array 'year' => 2013, 'price' => 39.50, 'active' => true, - 'generes' => ['animation', 'kids'], + 'genres' => ['animation', 'kids'], 'with-dash' => 'Works' ])); @@ -1656,7 +1844,7 @@ public function testFind(): array 'year' => 2019, 'price' => 39.50, 'active' => true, - 'generes' => ['animation', 'kids'], + 'genres' => ['animation', 'kids'], 'with-dash' => 'Works' ])); @@ -1680,7 +1868,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' ])); @@ -1704,7 +1892,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' ])); @@ -1728,7 +1916,7 @@ public function testFind(): array 'year' => 2025, 'price' => 0.0, 'active' => false, - 'generes' => [], + 'genres' => [], 'with-dash' => 'Works3' ])); @@ -1750,7 +1938,7 @@ public function testFind(): array 'year' => 2026, 'price' => 0.0, 'active' => false, - 'generes' => [], + 'genres' => [], 'with-dash' => 'Works3', 'nullable' => 'Not null' ])); @@ -1779,8 +1967,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 @@ -1950,7 +2138,7 @@ public function testFindContains(): void } $documents = static::getDatabase()->find('movies', [ - Query::contains('generes', ['comics']) + Query::contains('genres', ['comics']) ]); $this->assertEquals(2, count($documents)); @@ -1959,10 +2147,26 @@ 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)); + + $documents = static::getDatabase()->find('movies', [ + Query::contains('genres', ['non-existent']), + ]); + + $this->assertEquals(0, count($documents)); + + 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 @@ -4003,7 +4207,7 @@ public function testUniqueIndexDuplicate(): void 'year' => 2013, 'price' => 39.50, 'active' => true, - 'generes' => ['animation', 'kids'], + 'genres' => ['animation', 'kids'], 'with-dash' => 'Works4' ])); } @@ -4035,7 +4239,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/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; +// } +//} diff --git a/tests/unit/Validator/Query/FilterTest.php b/tests/unit/Validator/Query/FilterTest.php index 968d9a85d..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,6 +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_array', ['super']))); } public function testFailure(): void @@ -56,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