diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e69d87595..8f3298f77 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -98,5 +98,5 @@ jobs: sleep 10 - name: Run ${{matrix.service}} Tests - run: docker compose exec -T tests vendor/bin/phpunit /usr/src/code/tests/e2e/Adapter/${{matrix.adapter}}Test.php --debug || true + run: docker compose exec -T tests vendor/bin/phpunit /usr/src/code/tests/e2e/Adapter/${{matrix.adapter}}Test.php diff --git a/bin/tasks/index.php b/bin/tasks/index.php index d84e36d93..8ee4437b6 100644 --- a/bin/tasks/index.php +++ b/bin/tasks/index.php @@ -14,6 +14,7 @@ use Utopia\Database\Database; use Utopia\Database\Adapter\Mongo; use Utopia\Database\Adapter\MariaDB; +use Utopia\Database\Adapter\Ferret; use Utopia\Validator\Text; /** @@ -44,6 +45,19 @@ $database = new Database(new Mongo($client), $cache); break; + case 'ferretdb': + $client = new Client( + $name, + 'ferretdb', + 27017, + '', + '', + false + ); + + $database = new Database(new Ferret($client), $cache); + break; + case 'mariadb': $dbHost = 'mariadb'; $dbPort = '3306'; diff --git a/bin/tasks/load.php b/bin/tasks/load.php index a094292b3..988046501 100644 --- a/bin/tasks/load.php +++ b/bin/tasks/load.php @@ -22,6 +22,7 @@ use Utopia\Database\Document; use Utopia\Database\Adapter\Mongo; use Utopia\Database\Adapter\MariaDB; +use Utopia\Database\Adapter\Ferret; use Utopia\Validator\Numeric; use Utopia\Validator\Text; @@ -207,6 +208,46 @@ }); break; + case 'ferretdb': + Co\run(function () use (&$start, $limit, $name, $namespace, $cache) { + $client = new Client( + $name, + 'mongo', + 27017, + '', + '', + false + ); + + $database = new Database(new Ferret($client), $cache); + $database->setDefaultDatabase($name); + $database->setNamespace($namespace); + + // Outline collection schema + createSchema($database); + + // Fill DB + $faker = Factory::create(); + + $start = microtime(true); + + for ($i = 0; $i < $limit / 1000; $i++) { + go(function () use ($client, $faker, $name, $namespace, $cache) { + $database = new Database(new Mongo($client), $cache); + $database->setDefaultDatabase($name); + $database->setNamespace($namespace); + + // Each coroutine loads 1000 documents + for ($i = 0; $i < 1000; $i++) { + addArticle($database, $faker); + } + + $database = null; + }); + } + }); + break; + default: echo 'Adapter not supported'; return; diff --git a/bin/tasks/query.php b/bin/tasks/query.php index 4ece99e89..1c6ad392a 100644 --- a/bin/tasks/query.php +++ b/bin/tasks/query.php @@ -15,6 +15,7 @@ use Utopia\Database\Query; use Utopia\Database\Adapter\Mongo; use Utopia\Database\Adapter\MariaDB; +use Utopia\Database\Adapter\Ferret; use Utopia\Database\Validator\Authorization; use Utopia\Validator\Numeric; use Utopia\Validator\Text; @@ -49,6 +50,21 @@ $database->setNamespace($namespace); break; + case 'ferretdb': + $client = new Client( + $name, + 'ferret', + 27017, + '', + '', + false + ); + + $database = new Database(new Ferret($client), $cache); + $database->setDefaultDatabase($name); + $database->setNamespace($namespace); + break; + case 'mariadb': $dbHost = 'mariadb'; $dbPort = '3306'; diff --git a/composer.json b/composer.json index 9124bc56d..8360f6387 100755 --- a/composer.json +++ b/composer.json @@ -32,6 +32,12 @@ "check": "./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 512M", "coverage": "./vendor/bin/coverage-check ./tmp/clover.xml 90" }, + "repositories": [ + { + "type": "git", + "url": "https://github.com/utopia-php/mongo.git" + } + ], "require": { "ext-pdo": "*", "ext-mbstring": "*", diff --git a/composer.lock b/composer.lock index 4b582659b..b6fdb5749 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4adf56005e024ee6b5bb991e96e2ee72", + "content-hash": "f98993404ed32b3fdb23bee38579cb19", "packages": [ { "name": "jean85/pretty-package-versions", @@ -356,12 +356,6 @@ "url": "https://github.com/utopia-php/mongo.git", "reference": "52326a9a43e2d27ff0c15c48ba746dacbe9a7aee" }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/mongo/zipball/52326a9a43e2d27ff0c15c48ba746dacbe9a7aee", - "reference": "52326a9a43e2d27ff0c15c48ba746dacbe9a7aee", - "shasum": "" - }, "require": { "ext-mongodb": "*", "mongodb/mongodb": "1.10.0", @@ -380,7 +374,25 @@ "Utopia\\Mongo\\": "src" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "Utopia\\Tests\\": "tests" + } + }, + "scripts": { + "test": [ + "phpunit" + ], + "analyse": [ + "vendor/bin/phpstan analyse" + ], + "format": [ + "vendor/bin/pint" + ], + "lint": [ + "vendor/bin/pint --test" + ] + }, "license": [ "MIT" ], @@ -402,10 +414,6 @@ "upf", "utopia" ], - "support": { - "issues": "https://github.com/utopia-php/mongo/issues", - "source": "https://github.com/utopia-php/mongo/tree/0.3.1" - }, "time": "2023-09-01T17:25:28+00:00" } ], @@ -760,20 +768,21 @@ }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -814,9 +823,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -933,16 +948,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.30", + "version": "9.2.31", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", "shasum": "" }, "require": { @@ -999,7 +1014,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.30" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" }, "funding": [ { @@ -1007,7 +1022,7 @@ "type": "github" } ], - "time": "2023-12-22T06:47:57+00:00" + "time": "2024-03-02T06:37:42+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1454,16 +1469,16 @@ }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { @@ -1498,7 +1513,7 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" }, "funding": [ { @@ -1506,7 +1521,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -1752,16 +1767,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -1806,7 +1821,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -1814,7 +1829,7 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", @@ -1881,16 +1896,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", "shasum": "" }, "require": { @@ -1946,7 +1961,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" }, "funding": [ { @@ -1954,20 +1969,20 @@ "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2024-03-02T06:33:00+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.6", + "version": "5.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bde739e7565280bda77be70044ac1047bc007e34" + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", - "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", "shasum": "" }, "require": { @@ -2010,7 +2025,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" }, "funding": [ { @@ -2018,7 +2033,7 @@ "type": "github" } ], - "time": "2023-08-02T09:26:13+00:00" + "time": "2024-03-02T06:35:11+00:00" }, { "name": "sebastian/lines-of-code", @@ -2527,16 +2542,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -2565,7 +2580,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.2" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -2573,7 +2588,7 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2024-03-03T12:36:25+00:00" }, { "name": "utopia-php/cli", diff --git a/docker-compose.yml b/docker-compose.yml index 3dbd192eb..31ad023ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,6 +47,17 @@ services: environment: - MYSQL_ROOT_PASSWORD=password + ferretdb: + image: ghcr.io/ferretdb/ferretdb + container_name: utopia-ferretdb + restart: on-failure + networks: + - database + ports: + - "8799:27017" + environment: + - FERRETDB_POSTGRESQL_URL=postgres://root:password@postgres:5432/postgres + mongo: image: mongo:5.0 container_name: utopia-mongo 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/Ferret.php b/src/Database/Adapter/Ferret.php new file mode 100644 index 000000000..da74455d1 --- /dev/null +++ b/src/Database/Adapter/Ferret.php @@ -0,0 +1,102 @@ +getNamespace() . '_' . $this->filter($name); + + // Returns an array/object with the result document + if (empty($this->getClient()->createCollection($id))) { + return false; + } + + $indexesCreated = $this->client->createIndexes($id, [ + [ + 'key' => ['_uid' => $this->getOrder(Database::ORDER_DESC)], + 'name' => '_uid', + // 'unique' => true, + // 'collation' => [ // https://docs.mongodb.com/manual/core/index-case-insensitive/#create-a-case-insensitive-index + // 'locale' => 'en', + // 'strength' => 1, + // ] + ], + [ + 'key' => ['_read' => $this->getOrder(Database::ORDER_DESC)], + 'name' => '_read_permissions', + ] + ]); + + if (!$indexesCreated) { + return false; + } + + // Since attributes are not used by this adapter + // Only act when $indexes is provided + if (!empty($indexes)) { + /** + * Each new index has format ['key' => [$attribute => $order], 'name' => $name, 'unique' => $unique] + * @var array + */ + + $newIndexes = []; + + // using $i and $j as counters to distinguish from $key + foreach ($indexes as $i => $index) { + $key = []; + $name = $this->filter($index->getId()); + $unique = false; + + $attributes = $index->getAttribute('attributes'); + $orders = $index->getAttribute('orders'); + + foreach ($attributes as $j => $attribute) { + $attribute = $this->filter($attribute); + + switch ($index->getAttribute('type')) { + case Database::INDEX_KEY: + $order = $this->getOrder($this->filter($orders[$i] ?? Database::ORDER_ASC)); + break; + case Database::INDEX_FULLTEXT: + // MongoDB fulltext index is just 'text' + // Not using Database::INDEX_KEY for clarity + // $order = 'text'; + break; + case Database::INDEX_UNIQUE: + $order = $this->getOrder($this->filter($orders[$i] ?? Database::ORDER_ASC)); + $unique = true; + break; + default: + // index not supported + return false; + } + + if (isset($order)) { + $key[$attribute] = $order; + } + } + + $newIndexes[$i] = ['key' => $key, 'name' => $name, 'unique' => $unique]; + } + + if (!$this->getClient()->createIndexes($name, $newIndexes)) { + return false; + } + } + + return true; + } +} diff --git a/tests/e2e/Adapter/FerretDBTest.php b/tests/e2e/Adapter/FerretDBTest.php new file mode 100644 index 000000000..b6b21b69f --- /dev/null +++ b/tests/e2e/Adapter/FerretDBTest.php @@ -0,0 +1,75 @@ +connect('redis', 6379); + $redis->flushAll(); + $cache = new Cache(new RedisAdapter($redis)); + + $schema = 'utopiaTests'; // same as $this->testDatabase + $client = new Client( + $schema, + 'ferretdb', + 27017, + '', + '', + false + ); + + $database = new Database(new Ferret($client), $cache); + $database->setDatabase($schema); + $database->setNamespace('myapp_' . uniqid()); + + if ($database->exists('utopiaTests')) { + $database->delete('utopiaTests'); + } + + $database->create(); + + return self::$database = $database; + } + + public function testTrue(): void + { + $this->assertTrue(true); + } + + public function testCreateCollection(): void + { + $this->assertTrue(true); + } +}