diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 025894dd5..eea0e7842 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,14 +72,12 @@ jobs: matrix: adapter: [ - MongoDB, MariaDB, MySQL, Postgres, SQLite, Mirror, Pool, - SharedTables/MongoDB, SharedTables/MariaDB, SharedTables/MySQL, SharedTables/Postgres, diff --git a/Dockerfile b/Dockerfile index e33f09e71..381e801f7 100755 --- a/Dockerfile +++ b/Dockerfile @@ -16,11 +16,13 @@ FROM php:8.3.19-cli-alpine3.21 AS compile ENV PHP_REDIS_VERSION="6.0.2" \ PHP_SWOOLE_VERSION="v5.1.7" \ - PHP_XDEBUG_VERSION="3.4.2" \ - PHP_MONGODB_VERSION="2.1.1" + PHP_XDEBUG_VERSION="3.4.2" + RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone -RUN apk update && apk add --no-cache \ +RUN \ + apk update \ + && apk add --no-cache \ postgresql-libs \ postgresql-dev \ make \ @@ -33,11 +35,9 @@ RUN apk update && apk add --no-cache \ linux-headers \ docker-cli \ docker-cli-compose \ - && pecl install mongodb-$PHP_MONGODB_VERSION \ - && docker-php-ext-enable mongodb \ - && docker-php-ext-install opcache pgsql pdo_mysql pdo_pgsql \ - && apk del postgresql-dev \ - && rm -rf /var/cache/apk/* + && docker-php-ext-install opcache pgsql pdo_mysql pdo_pgsql \ + && apk del postgresql-dev \ + && rm -rf /var/cache/apk/* # Redis Extension FROM compile AS redis diff --git a/composer.json b/composer.json index 1ca56d8a4..4a0fecbd2 100755 --- a/composer.json +++ b/composer.json @@ -35,12 +35,10 @@ "require": { "php": ">=8.1", "ext-pdo": "*", - "ext-mongodb": "*", "ext-mbstring": "*", "utopia-php/framework": "0.33.*", "utopia-php/cache": "0.13.*", - "utopia-php/pools": "0.8.*", - "utopia-php/mongo": "0.10.*" + "utopia-php/pools": "0.8.*" }, "require-dev": { "fakerphp/faker": "1.23.*", @@ -48,15 +46,13 @@ "pcov/clobber": "2.*", "swoole/ide-helper": "5.1.3", "utopia-php/cli": "0.14.*", - "laravel/pint": "*", + "laravel/pint": "1.*", "phpstan/phpstan": "1.*", "rregeer/phpunit-coverage-check": "0.3.*" }, "suggests": { "ext-redis": "Needed to support Redis Cache Adapter", - "ext-pdo": "Needed to support MariaDB, MySQL or SQLite Database Adapter", - "mongodb/mongodb": "Needed to support MongoDB Database Adapter" - + "ext-pdo": "Needed to support MariaDB, MySQL or SQLite Database Adapter" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index 81cf8096c..5933f4fc9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,29 +4,29 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0b25c35427f7a3653f5c4993507316ee", + "content-hash": "5a68454fa54e1d31deef8571953a3da3", "packages": [ { "name": "brick/math", - "version": "0.14.0", + "version": "0.13.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", "shasum": "" }, "require": { - "php": "^8.2" + "php": "^8.1" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpstan/phpstan": "2.1.22", - "phpunit/phpunit": "^11.5" + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "6.8.8" }, "type": "library", "autoload": { @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.0" + "source": "https://github.com/brick/math/tree/0.13.1" }, "funding": [ { @@ -64,7 +64,7 @@ "type": "github" } ], - "time": "2025-08-29T12:40:03+00:00" + "time": "2025-03-29T13:50:30+00:00" }, { "name": "composer/semver", @@ -145,21 +145,24 @@ }, { "name": "google/protobuf", - "version": "v4.32.1", + "version": "v4.32.0", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb" + "reference": "9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb", - "reference": "c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646", + "reference": "9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646", "shasum": "" }, "require": { "php": ">=8.1.0" }, + "provide": { + "ext-protobuf": "*" + }, "require-dev": { "phpunit/phpunit": ">=5.0.0 <8.5.27" }, @@ -183,86 +186,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.32.1" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.32.0" }, - "time": "2025-09-14T05:14:52+00:00" - }, - { - "name": "mongodb/mongodb", - "version": "2.1.1", - "source": { - "type": "git", - "url": "https://github.com/mongodb/mongo-php-library.git", - "reference": "f399d24905dd42f97dfe0af9706129743ef247ac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mongodb/mongo-php-library/zipball/f399d24905dd42f97dfe0af9706129743ef247ac", - "reference": "f399d24905dd42f97dfe0af9706129743ef247ac", - "shasum": "" - }, - "require": { - "composer-runtime-api": "^2.0", - "ext-mongodb": "^2.1", - "php": "^8.1", - "psr/log": "^1.1.4|^2|^3", - "symfony/polyfill-php85": "^1.32" - }, - "replace": { - "mongodb/builder": "*" - }, - "require-dev": { - "doctrine/coding-standard": "^12.0", - "phpunit/phpunit": "^10.5.35", - "rector/rector": "^1.2", - "squizlabs/php_codesniffer": "^3.7", - "vimeo/psalm": "6.5.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "MongoDB\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Andreas Braun", - "email": "andreas.braun@mongodb.com" - }, - { - "name": "Jeremy Mikola", - "email": "jmikola@gmail.com" - }, - { - "name": "Jérôme Tamarelle", - "email": "jerome.tamarelle@mongodb.com" - } - ], - "description": "MongoDB driver library", - "homepage": "https://jira.mongodb.org/browse/PHPLIB", - "keywords": [ - "database", - "driver", - "mongodb", - "persistence" - ], - "support": { - "issues": "https://github.com/mongodb/mongo-php-library/issues", - "source": "https://github.com/mongodb/mongo-php-library/tree/2.1.1" - }, - "time": "2025-08-13T20:50:05+00:00" + "time": "2025-08-14T20:00:33+00:00" }, { "name": "nyholm/psr7", @@ -410,20 +336,20 @@ }, { "name": "open-telemetry/api", - "version": "1.6.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "ee17d937652eca06c2341b6fadc0f74c1c1a5af2" + "reference": "b3a9286f9c1c8247c83493c5b1fa475cd0cec7f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/ee17d937652eca06c2341b6fadc0f74c1c1a5af2", - "reference": "ee17d937652eca06c2341b6fadc0f74c1c1a5af2", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/b3a9286f9c1c8247c83493c5b1fa475cd0cec7f7", + "reference": "b3a9286f9c1c8247c83493c5b1fa475cd0cec7f7", "shasum": "" }, "require": { - "open-telemetry/context": "^1.4", + "open-telemetry/context": "^1.0", "php": "^8.1", "psr/log": "^1.1|^2.0|^3.0", "symfony/polyfill-php82": "^1.26" @@ -476,20 +402,20 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-09-19T00:05:49+00:00" + "time": "2025-06-19T23:36:51+00:00" }, { "name": "open-telemetry/context", - "version": "1.4.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/context.git", - "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf" + "reference": "438f71812242db3f196fb4c717c6f92cbc819be6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/d4c4470b541ce72000d18c339cfee633e4c8e0cf", - "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/438f71812242db3f196fb4c717c6f92cbc819be6", + "reference": "438f71812242db3f196fb4c717c6f92cbc819be6", "shasum": "" }, "require": { @@ -535,7 +461,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-09-19T00:05:49+00:00" + "time": "2025-08-13T01:12:00+00:00" }, { "name": "open-telemetry/exporter-otlp", @@ -603,16 +529,16 @@ }, { "name": "open-telemetry/gen-otlp-protobuf", - "version": "1.8.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git", - "reference": "673af5b06545b513466081884b47ef15a536edde" + "reference": "585bafddd4ae6565de154610b10a787a455c9ba0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/673af5b06545b513466081884b47ef15a536edde", - "reference": "673af5b06545b513466081884b47ef15a536edde", + "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/585bafddd4ae6565de154610b10a787a455c9ba0", + "reference": "585bafddd4ae6565de154610b10a787a455c9ba0", "shasum": "" }, "require": { @@ -662,27 +588,27 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-09-17T23:10:12+00:00" + "time": "2025-01-15T23:07:07+00:00" }, { "name": "open-telemetry/sdk", - "version": "1.8.0", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "105c6e81e3d86150bd5704b00c7e4e165e957b89" + "reference": "86287cf30fd6549444d7b8f7d8758d92e24086ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/105c6e81e3d86150bd5704b00c7e4e165e957b89", - "reference": "105c6e81e3d86150bd5704b00c7e4e165e957b89", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/86287cf30fd6549444d7b8f7d8758d92e24086ac", + "reference": "86287cf30fd6549444d7b8f7d8758d92e24086ac", "shasum": "" }, "require": { "ext-json": "*", "nyholm/psr7-server": "^1.1", - "open-telemetry/api": "^1.6", - "open-telemetry/context": "^1.4", + "open-telemetry/api": "~1.4.0", + "open-telemetry/context": "^1.0", "open-telemetry/sem-conv": "^1.0", "php": "^8.1", "php-http/discovery": "^1.14", @@ -759,7 +685,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-09-19T00:05:49+00:00" + "time": "2025-08-06T03:07:06+00:00" }, { "name": "open-telemetry/sem-conv", @@ -1238,20 +1164,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.1", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -1310,9 +1236,9 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.1" + "source": "https://github.com/ramsey/uuid/tree/4.9.0" }, - "time": "2025-09-04T20:59:21+00:00" + "time": "2025-06-25T14:20:11+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1383,16 +1309,16 @@ }, { "name": "symfony/http-client", - "version": "v7.3.4", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62" + "reference": "333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/4b62871a01c49457cf2a8e560af7ee8a94b87a62", - "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62", + "url": "https://api.github.com/repos/symfony/http-client/zipball/333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019", + "reference": "333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019", "shasum": "" }, "require": { @@ -1459,7 +1385,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.4" + "source": "https://github.com/symfony/http-client/tree/v7.3.3" }, "funding": [ { @@ -1479,7 +1405,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-08-27T07:45:05+00:00" }, { "name": "symfony/http-client-contracts", @@ -1804,86 +1730,6 @@ ], "time": "2025-07-08T02:45:35+00:00" }, - { - "name": "symfony/polyfill-php85", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", - "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php85\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-06-23T16:12:55+00:00" - }, { "name": "symfony/service-contracts", "version": "v3.6.0", @@ -2119,16 +1965,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.28", + "version": "0.33.24", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "5aaa94d406577b0059ad28c78022606890dc6de0" + "reference": "5112b1023342163e3fbedec99f38fc32c8700aa0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/5aaa94d406577b0059ad28c78022606890dc6de0", - "reference": "5aaa94d406577b0059ad28c78022606890dc6de0", + "url": "https://api.github.com/repos/utopia-php/http/zipball/5112b1023342163e3fbedec99f38fc32c8700aa0", + "reference": "5112b1023342163e3fbedec99f38fc32c8700aa0", "shasum": "" }, "require": { @@ -2160,70 +2006,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.28" + "source": "https://github.com/utopia-php/http/tree/0.33.24" }, - "time": "2025-09-25T10:44:24+00:00" - }, - { - "name": "utopia-php/mongo", - "version": "0.10.0", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/mongo.git", - "reference": "ecfad6aad2e2e3fe5899ac2ebf1009a21b4d6b18" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/mongo/zipball/ecfad6aad2e2e3fe5899ac2ebf1009a21b4d6b18", - "reference": "ecfad6aad2e2e3fe5899ac2ebf1009a21b4d6b18", - "shasum": "" - }, - "require": { - "ext-mongodb": "2.1.*", - "mongodb/mongodb": "2.1.*", - "php": ">=8.0", - "ramsey/uuid": "4.9.*" - }, - "require-dev": { - "fakerphp/faker": "1.*", - "laravel/pint": "*", - "phpstan/phpstan": "*", - "phpunit/phpunit": "9.*", - "swoole/ide-helper": "5.1.*" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\Mongo\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Eldad Fux", - "email": "eldad@appwrite.io" - }, - { - "name": "Wess", - "email": "wess@appwrite.io" - } - ], - "description": "A simple library to manage Mongo database", - "keywords": [ - "database", - "mongo", - "php", - "upf", - "utopia" - ], - "support": { - "issues": "https://github.com/utopia-php/mongo/issues", - "source": "https://github.com/utopia-php/mongo/tree/0.10.0" - }, - "time": "2025-10-02T04:50:07+00:00" + "time": "2025-09-04T04:18:39+00:00" }, { "name": "utopia-php/pools", @@ -2464,16 +2249,16 @@ }, { "name": "laravel/pint", - "version": "v1.25.1", + "version": "v1.24.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" + "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", + "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a", "shasum": "" }, "require": { @@ -2484,9 +2269,9 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.87.2", - "illuminate/view": "^11.46.0", - "larastan/larastan": "^3.7.1", + "friendsofphp/php-cs-fixer": "^3.82.2", + "illuminate/view": "^11.45.1", + "larastan/larastan": "^3.5.0", "laravel-zero/framework": "^11.45.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.3.1", @@ -2497,6 +2282,9 @@ ], "type": "project", "autoload": { + "files": [ + "overrides/Runner/Parallel/ProcessFactory.php" + ], "psr-4": { "App\\": "app/", "Database\\Seeders\\": "database/seeders/", @@ -2526,7 +2314,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-09-19T02:57:12+00:00" + "time": "2025-07-10T18:09:32+00:00" }, { "name": "myclabs/deep-copy", @@ -2798,11 +2586,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.32", + "version": "1.12.28", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9" + }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", - "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", + "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", "shasum": "" }, "require": { @@ -2847,7 +2640,7 @@ "type": "github" } ], - "time": "2025-09-30T10:16:31+00:00" + "time": "2025-07-17T17:15:39+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3170,16 +2963,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.29", + "version": "9.6.25", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" + "reference": "049c011e01be805202d8eebedef49f769a8ec7b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", - "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/049c011e01be805202d8eebedef49f769a8ec7b7", + "reference": "049c011e01be805202d8eebedef49f769a8ec7b7", "shasum": "" }, "require": { @@ -3204,7 +2997,7 @@ "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.8", + "sebastian/exporter": "^4.0.6", "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", @@ -3253,7 +3046,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.29" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.25" }, "funding": [ { @@ -3277,7 +3070,7 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:29:11+00:00" + "time": "2025-08-20T14:38:31+00:00" }, { "name": "rregeer/phpunit-coverage-check", @@ -3766,16 +3559,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.8", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", "shasum": "" }, "require": { @@ -3831,27 +3624,15 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", - "type": "tidelift" } ], - "time": "2025-09-24T06:03:27+00:00" + "time": "2024-03-02T06:33:00+00:00" }, { "name": "sebastian/global-state", @@ -4480,7 +4261,6 @@ "platform": { "php": ">=8.1", "ext-pdo": "*", - "ext-mongodb": "*", "ext-mbstring": "*" }, "platform-dev": {}, diff --git a/docker-compose.yml b/docker-compose.yml index 098d465ac..1b24d8496 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -83,55 +83,8 @@ services: environment: - MYSQL_ROOT_PASSWORD=password - mongo: - image: mongo:8.0.14 - container_name: utopia-mongo - entrypoint: ["/entrypoint.sh"] - networks: - - database - ports: - - "9706:27017" - volumes: - - mongo-data:/data/db - - ./tests/resources/mongo/mongo-keyfile:/tmp/keyfile:ro - - ./tests/resources/mongo/entrypoint.sh:/entrypoint.sh:ro - environment: - MONGO_INITDB_ROOT_USERNAME: root - MONGO_INITDB_ROOT_PASSWORD: password - MONGO_INITDB_DATABASE: utopia_testing - healthcheck: - test: | - bash -c " - if mongosh -u root -p password --authenticationDatabase admin --quiet --eval 'rs.status().ok' 2>/dev/null; then - exit 0 - else - mongosh -u root -p password --authenticationDatabase admin --quiet --eval \" - rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'localhost:27017'}]}) - \" 2>/dev/null || exit 1 - fi - " - interval: 10s - timeout: 10s - retries: 10 - start_period: 30s - - mongo-express: - image: mongo-express - container_name: mongo-express - depends_on: - mongo: - condition: service_healthy - networks: - - database - ports: - - "8083:8081" - environment: - ME_CONFIG_MONGODB_URL: mongodb://root:password@mongo:27017/?authSource=admin&directConnection=true - ME_CONFIG_BASICAUTH_USERNAME: admin - ME_CONFIG_BASICAUTH_PASSWORD: admin - mysql: - image: mysql:8.0.43 + image: mysql:8.0.41 container_name: utopia-mysql networks: - database @@ -147,7 +100,7 @@ services: - SYS_NICE mysql-mirror: - image: mysql:8.0.43 + image: mysql:8.0.41 container_name: utopia-mysql-mirror networks: - database @@ -163,7 +116,7 @@ services: - SYS_NICE redis: - image: redis:8.2.1-alpine3.22 + image: redis:7.4.1-alpine3.20 container_name: utopia-redis ports: - "8708:6379" @@ -171,20 +124,12 @@ services: - database redis-mirror: - image: redis:8.2.1-alpine3.22 + image: redis:7.4.1-alpine3.20 container_name: utopia-redis-mirror ports: - "8709:6379" networks: - database -volumes: - mongo-data: - networks: database: - -secrets: - mongo_keyfile: - file: ./tests/resources/mongo/mongo-keyfile - diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 172f7bd1b..2332d9745 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1098,28 +1098,6 @@ abstract public function getSupportForBoundaryInclusiveContains(): bool; */ abstract public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool; - /** - * Does the adapter support multiple fulltext indexes? - * - * @return bool - */ - abstract public function getSupportForMultipleFulltextIndexes(): bool; - - - /** - * Does the adapter support identical indexes? - * - * @return bool - */ - abstract public function getSupportForIdenticalIndexes(): bool; - - /** - * Does the adapter support random order by? - * - * @return bool - */ - abstract public function getSupportForOrderRandom(): bool; - /** * Get current attribute count from collection document * @@ -1181,9 +1159,9 @@ abstract public function getKeywords(): array; * * @param array $selections * @param string $prefix - * @return mixed + * @return string */ - abstract protected function getAttributeProjection(array $selections, string $prefix): mixed; + abstract protected function getAttributeProjection(array $selections, string $prefix): string; /** * Get all selected attributes from queries @@ -1337,43 +1315,4 @@ abstract public function decodeLinestring(string $wkb): array; * @return float[][][] Array of rings, each ring is an array of points [x, y] */ abstract public function decodePolygon(string $wkb): array; - - /** - * Returns the document after casting - * @param Document $collection - * @param Document $document - * @return Document - */ - abstract public function castingBefore(Document $collection, Document $document): Document; - - /** - * Returns the document after casting - * @param Document $collection - * @param Document $document - * @return Document - */ - abstract public function castingAfter(Document $collection, Document $document): Document; - - /** - * Is internal casting supported? - * - * @return bool - */ - abstract public function getSupportForInternalCasting(): bool; - - /** - * Is UTC casting supported? - * - * @return bool - */ - abstract public function getSupportForUTCCasting(): bool; - - /** - * Set UTC Datetime - * - * @param string $value - * @return mixed - */ - abstract public function setUTCDatetime(string $value): mixed; - } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php deleted file mode 100644 index 25528ef5f..000000000 --- a/src/Database/Adapter/Mongo.php +++ /dev/null @@ -1,3088 +0,0 @@ - - */ - private array $operators = [ - '$eq', - '$ne', - '$lt', - '$lte', - '$gt', - '$gte', - '$in', - '$nin', - '$text', - '$search', - '$or', - '$and', - '$match', - '$regex', - '$not', - '$nor', - ]; - - protected Client $client; - - /** - * Default batch size for cursor operations - */ - private const DEFAULT_BATCH_SIZE = 1000; - - /** - * Transaction/session state for MongoDB transactions - * @var array|null $session - */ - private ?array $session = null; // Store session array from startSession - protected int $inTransaction = 0; - - /** - * Constructor. - * - * Set connection and settings - * - * @param Client $client - * @throws MongoException - */ - public function __construct(Client $client) - { - $this->client = $client; - $this->client->connect(); - } - - public function setTimeout(int $milliseconds, string $event = Database::EVENT_ALL): void - { - if (!$this->getSupportForTimeouts()) { - return; - } - - $this->timeout = $milliseconds; - } - - public function clearTimeout(string $event): void - { - parent::clearTimeout($event); - - $this->timeout = 0; - } - - /** - * @template T - * @param callable(): T $callback - * @return T - * @throws \Throwable - */ - public function withTransaction(callable $callback): mixed - { - // If the database is not a replica set, we can't use transactions - if (!$this->client->isReplicaSet()) { - $result = $callback(); - return $result; - } - - try { - $this->startTransaction(); - $result = $callback(); - $this->commitTransaction(); - return $result; - } catch (\Throwable $action) { - try { - $this->rollbackTransaction(); - } catch (\Throwable) { - // Throw the original exception, not the rollback one - // Since if it's a duplicate key error, the rollback will fail, - // and we want to throw the original exception. - } finally { - // Ensure state is cleaned up even if rollback fails - $this->inTransaction = 0; - $this->session = null; - } - - throw $action; - } - } - - public function startTransaction(): bool - { - // If the database is not a replica set, we can't use transactions - if (!$this->client->isReplicaSet()) { - return true; - } - - try { - if ($this->inTransaction === 0) { - if (!$this->session) { - $this->session = $this->client->startSession(); // Get session array - $this->client->startTransaction($this->session); // Start the transaction - } - } - $this->inTransaction++; - return true; - } catch (\Throwable $e) { - $this->session = null; - $this->inTransaction = 0; - throw new DatabaseException('Failed to start transaction: ' . $e->getMessage(), $e->getCode(), $e); - } - } - - public function commitTransaction(): bool - { - // If the database is not a replica set, we can't use transactions - if (!$this->client->isReplicaSet()) { - return true; - } - - try { - if ($this->inTransaction === 0) { - return false; - } - $this->inTransaction--; - if ($this->inTransaction === 0) { - if (!$this->session) { - return false; - } - try { - $result = $this->client->commitTransaction($this->session); - } catch (MongoException $e) { - // If there's no active transaction, it may have been auto-aborted due to an error. - // This is not necessarily a failure, just return success since the transaction was already terminated. - $e = $this->processException($e); - if ($e instanceof TransactionException) { - $this->session = null; - $this->inTransaction = 0; // Reset counter when transaction is already terminated - return true; - } - throw $e; - } catch (\Throwable $e) { - throw new DatabaseException($e->getMessage(), $e->getCode(), $e); - } finally { - $this->session = null; - } - - return true; - } - return true; - } catch (\Throwable $e) { - // Ensure cleanup on any failure - $this->session = null; - $this->inTransaction = 0; - throw new DatabaseException('Failed to commit transaction: ' . $e->getMessage(), $e->getCode(), $e); - } - } - - public function rollbackTransaction(): bool - { - // If the database is not a replica set, we can't use transactions - if (!$this->client->isReplicaSet()) { - return true; - } - - try { - if ($this->inTransaction === 0) { - return false; - } - $this->inTransaction--; - if ($this->inTransaction === 0) { - if (!$this->session) { - return false; - } - - try { - $result = $this->client->abortTransaction($this->session); - } catch (\Throwable $e) { - throw new DatabaseException($e->getMessage(), $e->getCode(), $e); - } finally { - $this->session = null; - } - - return true; - } - return true; - } catch (\Throwable $e) { - $this->session = null; - $this->inTransaction = 0; - throw new DatabaseException('Failed to rollback transaction: ' . $e->getMessage(), $e->getCode(), $e); - } - } - - /** - * Helper to add transaction/session context to command options if in transaction - * Includes defensive check to ensure session is valid - * - * @param array $options - * @return array - */ - private function getTransactionOptions(array $options = []): array - { - if ($this->inTransaction > 0 && $this->session !== null) { - // Pass the session array directly - the client will handle the transaction state internally - $options['session'] = $this->session; - } - return $options; - } - - - /** - * Create a safe MongoDB regex pattern by escaping special characters - * - * @param string $value The user input to escape - * @param string $pattern The pattern template (e.g., ".*%s.*" for contains) - * @return Regex - * @throws DatabaseException - */ - private function createSafeRegex(string $value, string $pattern = '%s', string $flags = 'i'): Regex - { - $escaped = preg_quote($value, '/'); - - // Validate that the pattern doesn't contain injection vectors - if (preg_match('/\$[a-z]+/i', $escaped)) { - throw new DatabaseException('Invalid regex pattern: potential injection detected'); - } - - $finalPattern = sprintf($pattern, $escaped); - - return new Regex($finalPattern, $flags); - } - - /** - * Ping Database - * - * @return bool - * @throws Exception - * @throws MongoException - */ - public function ping(): bool - { - return $this->getClient()->query([ - 'ping' => 1, - 'skipReadConcern' => true - ])->ok ?? false; - } - - public function reconnect(): void - { - $this->client->connect(); - } - - /** - * Create Database - * - * @param string $name - * - * @return bool - */ - public function create(string $name): bool - { - return true; - } - - /** - * Check if database exists - * Optionally check if collection exists in database - * - * @param string $database database name - * @param string|null $collection (optional) collection name - * - * @return bool - * @throws Exception - */ - public function exists(string $database, ?string $collection = null): bool - { - if (!\is_null($collection)) { - $collection = $this->getNamespace() . "_" . $collection; - try { - // Use listCollections command with filter for O(1) lookup - $result = $this->getClient()->query([ - 'listCollections' => 1, - 'filter' => ['name' => $collection] - ]); - - return !empty($result->cursor->firstBatch); - } catch (\Exception $e) { - return false; - } - } - - return $this->getClient()->selectDatabase() != null; - } - - /** - * List Databases - * - * @return array - * @throws Exception - */ - public function list(): array - { - $list = []; - - foreach ((array)$this->getClient()->listDatabaseNames() as $value) { - $list[] = $value; - } - - return $list; - } - - /** - * Delete Database - * - * @param string $name - * - * @return bool - * @throws Exception - */ - public function delete(string $name): bool - { - $this->getClient()->dropDatabase([], $name); - - return true; - } - - /** - * Create Collection - * - * @param string $name - * @param array $attributes - * @param array $indexes - * @return bool - * @throws Exception - */ - public function createCollection(string $name, array $attributes = [], array $indexes = []): bool - { - $id = $this->getNamespace() . '_' . $this->filter($name); - - // For metadata collections outside transactions, check if exists first - if (!$this->inTransaction && $name === Database::METADATA && $this->exists($this->getNamespace(), $name)) { - return true; - } - - // Returns an array/object with the result document - try { - $options = $this->getTransactionOptions(); - $this->getClient()->createCollection($id, $options); - - } catch (MongoException $e) { - $processed = $this->processException($e); - if ($processed instanceof DuplicateException) { - return true; - } - throw $processed; - } - - $internalIndex = [ - [ - 'key' => ['_uid' => $this->getOrder(Database::ORDER_ASC)], - 'name' => '_uid', - 'unique' => true, - 'collation' => [ - 'locale' => 'en', - 'strength' => 1, - ] - ], - [ - 'key' => ['_createdAt' => $this->getOrder(Database::ORDER_ASC)], - 'name' => '_createdAt', - ], - [ - 'key' => ['_updatedAt' => $this->getOrder(Database::ORDER_ASC)], - 'name' => '_updatedAt', - ], - [ - 'key' => ['_permissions' => $this->getOrder(Database::ORDER_ASC)], - 'name' => '_permissions', - ] - ]; - - if ($this->sharedTables) { - foreach ($internalIndex as &$index) { - $index['key'] = array_merge(['_tenant' => $this->getOrder(Database::ORDER_ASC)], $index['key']); - } - unset($index); - } - - try { - $options = $this->getTransactionOptions(); - $indexesCreated = $this->client->createIndexes($id, $internalIndex, $options); - } catch (\Exception $e) { - throw $this->processException($e); - } - - 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] - */ - $newIndexes = []; - - $collectionAttributes = $attributes; - - // using $i and $j as counters to distinguish from $key - foreach ($indexes as $i => $index) { - - $key = []; - $unique = false; - $attributes = $index->getAttribute('attributes'); - $orders = $index->getAttribute('orders'); - - // If sharedTables, always add _tenant as the first key - if ($this->sharedTables) { - $key['_tenant'] = $this->getOrder(Database::ORDER_ASC); - } - - foreach ($attributes as $j => $attribute) { - $attribute = $this->filter($this->getInternalKeyForAttribute($attribute)); - - switch ($index->getAttribute('type')) { - case Database::INDEX_KEY: - $order = $this->getOrder($this->filter($orders[$j] ?? 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[$j] ?? Database::ORDER_ASC)); - $unique = true; - break; - default: - // index not supported - return false; - } - - $key[$attribute] = $order; - } - - $newIndexes[$i] = [ - 'key' => $key, - 'name' => $this->filter($index->getId()), - 'unique' => $unique - ]; - - if ($index->getAttribute('type') === Database::INDEX_FULLTEXT) { - $newIndexes[$i]['default_language'] = 'none'; - } - - // Add partial filter for indexes to avoid indexing null values - if (in_array($index->getAttribute('type'), [ - Database::INDEX_UNIQUE, - Database::INDEX_KEY - ])) { - $partialFilter = []; - foreach ($attributes as $attr) { - // Find the matching attribute in collectionAttributes to get its type - $attrType = 'string'; // Default fallback - foreach ($collectionAttributes as $collectionAttr) { - if ($collectionAttr->getId() === $attr) { - $attrType = $this->getMongoTypeCode($collectionAttr->getAttribute('type')); - break; - } - } - - $attr = $this->filter($this->getInternalKeyForAttribute($attr)); - - // Use both $exists: true and $type to exclude nulls and ensure correct type - $partialFilter[$attr] = [ - '$exists' => true, - '$type' => $attrType - ]; - } - if (!empty($partialFilter)) { - $newIndexes[$i]['partialFilterExpression'] = $partialFilter; - } - } - } - - try { - $options = $this->getTransactionOptions(); - $indexesCreated = $this->getClient()->createIndexes($id, $newIndexes, $options); - } catch (\Exception $e) { - throw $this->processException($e); - } - - if (!$indexesCreated) { - return false; - } - } - - return true; - } - - /** - * List Collections - * - * @return array - * @throws Exception - */ - public function listCollections(): array - { - $list = []; - - // Note: listCollections is a metadata operation that should not run in transactions - // to avoid transaction conflicts and readConcern issues - foreach ((array)$this->getClient()->listCollectionNames() as $value) { - $list[] = $value; - } - - return $list; - } - - /** - * Get Collection Size on disk - * @param string $collection - * @return int - * @throws DatabaseException - */ - public function getSizeOfCollectionOnDisk(string $collection): int - { - return $this->getSizeOfCollection($collection); - } - - /** - * Get Collection Size of raw data - * @param string $collection - * @return int - * @throws DatabaseException - */ - public function getSizeOfCollection(string $collection): int - { - $namespace = $this->getNamespace(); - $collection = $this->filter($collection); - $collection = $namespace . '_' . $collection; - - $command = [ - 'collStats' => $collection, - 'scale' => 1 - ]; - - try { - $result = $this->getClient()->query($command); - if (is_object($result)) { - return $result->totalSize; - } else { - throw new DatabaseException('No size found'); - } - } catch (Exception $e) { - throw new DatabaseException('Failed to get collection size: ' . $e->getMessage()); - } - } - - /** - * Delete Collection - * - * @param string $id - * @return bool - * @throws Exception - */ - public function deleteCollection(string $id): bool - { - $id = $this->getNamespace() . '_' . $this->filter($id); - return (!!$this->getClient()->dropCollection($id)); - } - - /** - * Analyze a collection updating it's metadata on the database engine - * - * @param string $collection - * @return bool - */ - public function analyzeCollection(string $collection): bool - { - return false; - } - - /** - * Create Attribute - * - * @param string $collection - * @param string $id - * @param string $type - * @param int $size - * @param bool $signed - * @param bool $array - * @return bool - */ - public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): bool - { - return true; - } - - /** - * Create Attributes - * - * @param string $collection - * @param array> $attributes - * @return bool - * @throws DatabaseException - */ - public function createAttributes(string $collection, array $attributes): bool - { - return true; - } - - /** - * Delete Attribute - * - * @param string $collection - * @param string $id - * - * @return bool - * @throws DatabaseException - * @throws MongoException - */ - public function deleteAttribute(string $collection, string $id): bool - { - $collection = $this->getNamespace() . '_' . $this->filter($collection); - - $this->getClient()->update( - $collection, - [], - ['$unset' => [$id => '']], - multi: true - ); - - return true; - } - - /** - * Rename Attribute. - * - * @param string $collection - * @param string $id - * @param string $name - * @return bool - * @throws DatabaseException - * @throws MongoException - */ - public function renameAttribute(string $collection, string $id, string $name): bool - { - $collection = $this->getNamespace() . '_' . $this->filter($collection); - - $from = $this->filter($this->getInternalKeyForAttribute($id)); - $to = $this->filter($this->getInternalKeyForAttribute($name)); - $options = $this->getTransactionOptions(); - - $this->getClient()->update( - $collection, - [], - ['$rename' => [$from => $to]], - multi: true, - options: $options - ); - - return true; - } - - /** - * @param string $collection - * @param string $relatedCollection - * @param string $type - * @param bool $twoWay - * @param string $id - * @param string $twoWayKey - * @return bool - */ - public function createRelationship(string $collection, string $relatedCollection, string $type, bool $twoWay = false, string $id = '', string $twoWayKey = ''): bool - { - return true; - } - - /** - * @param string $collection - * @param string $relatedCollection - * @param string $type - * @param bool $twoWay - * @param string $key - * @param string $twoWayKey - * @param string $side - * @param string|null $newKey - * @param string|null $newTwoWayKey - * @return bool - * @throws DatabaseException - * @throws MongoException - */ - public function updateRelationship( - string $collection, - string $relatedCollection, - string $type, - bool $twoWay, - string $key, - string $twoWayKey, - string $side, - ?string $newKey = null, - ?string $newTwoWayKey = null - ): bool { - $collection = $this->getNamespace() . '_' . $this->filter($collection); - $relatedCollection = $this->getNamespace() . '_' . $this->filter($relatedCollection); - - $renameKey = [ - '$rename' => [ - $key => $newKey, - ] - ]; - - $renameTwoWayKey = [ - '$rename' => [ - $twoWayKey => $newTwoWayKey, - ] - ]; - - switch ($type) { - case Database::RELATION_ONE_TO_ONE: - if (!\is_null($newKey)) { - $this->getClient()->update($collection, updates: $renameKey, multi: true); - } - if ($twoWay && !\is_null($newTwoWayKey)) { - $this->getClient()->update($relatedCollection, updates: $renameTwoWayKey, multi: true); - } - break; - case Database::RELATION_ONE_TO_MANY: - if ($twoWay && !\is_null($newTwoWayKey)) { - $this->getClient()->update($relatedCollection, updates: $renameTwoWayKey, multi: true); - } - break; - case Database::RELATION_MANY_TO_ONE: - if (!\is_null($newKey)) { - $this->getClient()->update($collection, updates: $renameKey, multi: true); - } - break; - case Database::RELATION_MANY_TO_MANY: - $metadataCollection = new Document(['$id' => Database::METADATA]); - $collection = $this->getDocument($metadataCollection, $collection); - $relatedCollection = $this->getDocument($metadataCollection, $relatedCollection); - - if ($collection->isEmpty() || $relatedCollection->isEmpty()) { - throw new DatabaseException('Collection or related collection not found'); - } - - $junction = $this->getNamespace() . '_' . $this->filter('_' . $collection->getSequence() . '_' . $relatedCollection->getSequence()); - - if (!\is_null($newKey)) { - $this->getClient()->update($junction, updates: $renameKey, multi: true); - } - if ($twoWay && !\is_null($newTwoWayKey)) { - $this->getClient()->update($junction, updates: $renameTwoWayKey, multi: true); - } - break; - default: - throw new DatabaseException('Invalid relationship type'); - } - - return true; - } - - /** - * @param string $collection - * @param string $relatedCollection - * @param string $type - * @param bool $twoWay - * @param string $key - * @param string $twoWayKey - * @param string $side - * @return bool - * @throws MongoException - * @throws Exception - */ - public function deleteRelationship( - string $collection, - string $relatedCollection, - string $type, - bool $twoWay, - string $key, - string $twoWayKey, - string $side - ): bool { - $junction = $this->getNamespace() . '_' . $this->filter('_' . $collection . '_' . $relatedCollection); - $collection = $this->getNamespace() . '_' . $this->filter($collection); - $relatedCollection = $this->getNamespace() . '_' . $this->filter($relatedCollection); - - switch ($type) { - case Database::RELATION_ONE_TO_ONE: - $this->getClient()->update($collection, [], ['$unset' => [$key => '']], multi: true); - if ($twoWay) { - $this->getClient()->update($relatedCollection, [], ['$unset' => [$twoWayKey => '']], multi: true); - } - break; - case Database::RELATION_ONE_TO_MANY: - if ($side === Database::RELATION_SIDE_PARENT) { - $this->getClient()->update($collection, [], ['$unset' => [$key => '']], multi: true); - } else { - $this->getClient()->update($relatedCollection, [], ['$unset' => [$twoWayKey => '']], multi: true); - } - break; - case Database::RELATION_MANY_TO_ONE: - if ($side === Database::RELATION_SIDE_CHILD) { - $this->getClient()->update($collection, [], ['$unset' => [$key => '']], multi: true); - } else { - $this->getClient()->update($relatedCollection, [], ['$unset' => [$twoWayKey => '']], multi: true); - } - break; - case Database::RELATION_MANY_TO_MANY: - $this->getClient()->dropCollection($junction); - break; - default: - throw new DatabaseException('Invalid relationship type'); - } - - return true; - } - - /** - * Create Index - * - * @param string $collection - * @param string $id - * @param string $type - * @param array $attributes - * @param array $lengths - * @param array $orders - * @param array $indexAttributeTypes - * @param array $collation - * @return bool - * @throws Exception - */ - public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders, array $indexAttributeTypes = [], array $collation = []): bool - { - $name = $this->getNamespace() . '_' . $this->filter($collection); - $id = $this->filter($id); - $indexes = []; - $options = []; - $indexes['name'] = $id; - - // If sharedTables, always add _tenant as the first key - if ($this->sharedTables) { - $indexes['key']['_tenant'] = $this->getOrder(Database::ORDER_ASC); - } - - foreach ($attributes as $i => $attribute) { - - $attributes[$i] = $this->filter($this->getInternalKeyForAttribute($attribute)); - - $orderType = $this->getOrder($this->filter($orders[$i] ?? Database::ORDER_ASC)); - $indexes['key'][$attributes[$i]] = $orderType; - - switch ($type) { - case Database::INDEX_KEY: - break; - case Database::INDEX_FULLTEXT: - $indexes['key'][$attributes[$i]] = 'text'; - break; - case Database::INDEX_UNIQUE: - $indexes['unique'] = true; - break; - default: - return false; - } - } - - /** - * Collation - * 1. Moved under $indexes. - * 2. Updated format. - * 3. Avoid adding collation to fulltext index - */ - if (!empty($collation) && - $type !== Database::INDEX_FULLTEXT) { - $indexes['collation'] = [ - 'locale' => 'en', - 'strength' => 1, - ]; - } - - /** - * Text index language configuration - * Set to 'none' to disable stop words (words like 'other', 'the', 'a', etc.) - * This ensures all words are indexed and searchable - */ - if ($type === Database::INDEX_FULLTEXT) { - $indexes['default_language'] = 'none'; - } - - // Add partial filter for indexes to avoid indexing null values - if (in_array($type, [Database::INDEX_UNIQUE, Database::INDEX_KEY])) { - $partialFilter = []; - foreach ($attributes as $i => $attr) { - $attrType = $indexAttributeTypes[$i] ?? Database::VAR_STRING; // Default to string if type not provided - $attrType = $this->getMongoTypeCode($attrType); - $partialFilter[$attr] = ['$exists' => true, '$type' => $attrType]; - } - if (!empty($partialFilter)) { - $indexes['partialFilterExpression'] = $partialFilter; - } - } - try { - $result = $this->client->createIndexes($name, [$indexes], $options); - - // Wait for unique index to be fully built before returning - // MongoDB builds indexes asynchronously, so we need to wait for completion - // to ensure unique constraints are enforced immediately - if ($type === Database::INDEX_UNIQUE) { - $maxRetries = 10; - $retryCount = 0; - $baseDelay = 50000; // 50ms - $maxDelay = 500000; // 500ms - - while ($retryCount < $maxRetries) { - try { - $indexList = $this->client->query([ - 'listIndexes' => $name - ]); - - if (isset($indexList->cursor->firstBatch)) { - foreach ($indexList->cursor->firstBatch as $existingIndex) { - $indexArray = $this->client->toArray($existingIndex); - - if ( - (isset($indexArray['name']) && $indexArray['name'] === $id) && - (!isset($indexArray['buildState']) || $indexArray['buildState'] === 'ready') - ) { - return $result; - } - } - } - } catch (\Exception $e) { - if ($retryCount >= $maxRetries - 1) { - throw new DatabaseException( - 'Timeout waiting for index creation: ' . $e->getMessage(), - $e->getCode(), - $e - ); - } - } - - $delay = \min($baseDelay * (2 ** $retryCount), $maxDelay); - \usleep((int)$delay); - $retryCount++; - } - - throw new DatabaseException("Index {$id} creation timed out after {$maxRetries} retries"); - } - - return $result; - } catch (\Exception $e) { - throw $this->processException($e); - } - } - - /** - * Rename Index. - * - * @param string $collection - * @param string $old - * @param string $new - * - * @return bool - * @throws Exception - */ - public function renameIndex(string $collection, string $old, string $new): bool - { - $collection = $this->filter($collection); - $metadataCollection = new Document(['$id' => Database::METADATA]); - $collectionDocument = $this->getDocument($metadataCollection, $collection); - $old = $this->filter($old); - $new = $this->filter($new); - $indexes = json_decode($collectionDocument['indexes'], true); - $index = null; - - foreach ($indexes as $node) { - if ($node['key'] === $old) { - $index = $node; - break; - } - } - - // Extract attribute types from the collection document - $indexAttributeTypes = []; - if (isset($collectionDocument['attributes'])) { - $attributes = json_decode($collectionDocument['attributes'], true); - if ($attributes && $index) { - // Map index attributes to their types - foreach ($index['attributes'] as $attrName) { - foreach ($attributes as $attr) { - if ($attr['key'] === $attrName) { - $indexAttributeTypes[$attrName] = $attr['type']; - break; - } - } - } - } - } - - try { - $deletedindex = $this->deleteIndex($collection, $old); - $createdindex = $this->createIndex($collection, $new, $index['type'], $index['attributes'], $index['lengths'] ?? [], $index['orders'] ?? [], $indexAttributeTypes); - } catch (\Exception $e) { - throw $this->processException($e); - } - - if ($index && $deletedindex && $createdindex) { - return true; - } - - return false; - } - - /** - * Delete Index - * - * @param string $collection - * @param string $id - * - * @return bool - * @throws Exception - */ - public function deleteIndex(string $collection, string $id): bool - { - $name = $this->getNamespace() . '_' . $this->filter($collection); - $id = $this->filter($id); - $this->getClient()->dropIndexes($name, [$id]); - - return true; - } - - /** - * Get Document - * - * @param Document $collection - * @param string $id - * @param Query[] $queries - * @param bool $forUpdate - * @return Document - * @throws DatabaseException - */ - public function getDocument(Document $collection, string $id, array $queries = [], bool $forUpdate = false): Document - { - $name = $this->getNamespace() . '_' . $this->filter($collection->getId()); - - $filters = ['_uid' => $id]; - - if ($this->sharedTables) { - $filters['_tenant'] = $this->getTenantFilters($collection->getId()); - } - - - $options = []; - - $selections = $this->getAttributeSelections($queries); - - if (!empty($selections) && !\in_array('*', $selections)) { - $options['projection'] = $this->getAttributeProjection($selections); - } - - try { - $result = $this->client->find($name, $filters, $options)->cursor->firstBatch; - } catch (MongoException $e) { - throw $this->processException($e); - } - - if (empty($result)) { - return new Document([]); - } - - $result = $this->replaceChars('_', '$', (array)$result[0]); - - return new Document($result); - } - - /** - * Create Document - * - * @param Document $collection - * @param Document $document - * - * @return Document - * @throws Exception - */ - public function createDocument(Document $collection, Document $document): Document - { - $name = $this->getNamespace() . '_' . $this->filter($collection->getId()); - - $sequence = $document->getSequence(); - - $document->removeAttribute('$sequence'); - - if ($this->sharedTables) { - $document->setAttribute('$tenant', $this->getTenant()); - } - - $record = $this->replaceChars('$', '_', (array)$document); - - // Insert manual id if set - if (!empty($sequence)) { - $record['_id'] = $sequence; - } - $options = $this->getTransactionOptions(); - $result = $this->insertDocument($name, $this->removeNullKeys($record), $options); - $result = $this->replaceChars('_', '$', $result); - // in order to keep the original object refrence. - foreach ($result as $key => $value) { - $document->setAttribute($key, $value); - } - - return $document; - } - - /** - * Returns the document after casting from - * @param Document $collection - * @param Document $document - * @return Document - */ - public function castingAfter(Document $collection, Document $document): Document - { - if (!$this->getSupportForInternalCasting()) { - return $document; - } - - if ($document->isEmpty()) { - return $document; - } - - $attributes = $collection->getAttribute('attributes', []); - - $attributes = \array_merge($attributes, Database::INTERNAL_ATTRIBUTES); - - foreach ($attributes as $attribute) { - $key = $attribute['$id'] ?? ''; - $type = $attribute['type'] ?? ''; - $array = $attribute['array'] ?? false; - $value = $document->getAttribute($key); - if (is_null($value)) { - continue; - } - - if ($array) { - if (is_string($value)) { - $decoded = json_decode($value, true); - if (json_last_error() !== JSON_ERROR_NONE) { - throw new DatabaseException('Failed to decode JSON for attribute ' . $key . ': ' . json_last_error_msg()); - } - $value = $decoded; - } - } else { - $value = [$value]; - } - - foreach ($value as &$node) { - switch ($type) { - case Database::VAR_INTEGER: - $node = (int)$node; - break; - case Database::VAR_DATETIME : - if ($node instanceof UTCDateTime) { - $node = DateTime::format($node->toDateTime()); - } - break; - default: - break; - } - } - unset($node); - $document->setAttribute($key, ($array) ? $value : $value[0]); - } - - return $document; - } - - /** - * Returns the document after casting to - * @param Document $collection - * @param Document $document - * @return Document - * @throws Exception - */ - public function castingBefore(Document $collection, Document $document): Document - { - if (!$this->getSupportForInternalCasting()) { - return $document; - } - - if ($document->isEmpty()) { - return $document; - } - - $attributes = $collection->getAttribute('attributes', []); - - $attributes = \array_merge($attributes, Database::INTERNAL_ATTRIBUTES); - - foreach ($attributes as $attribute) { - $key = $attribute['$id'] ?? ''; - $type = $attribute['type'] ?? ''; - $array = $attribute['array'] ?? false; - - $value = $document->getAttribute($key); - if (is_null($value)) { - continue; - } - - if ($array) { - if (is_string($value)) { - $decoded = json_decode($value, true); - if (json_last_error() !== JSON_ERROR_NONE) { - throw new DatabaseException('Failed to decode JSON for attribute ' . $key . ': ' . json_last_error_msg()); - } - $value = $decoded; - } - } else { - $value = [$value]; - } - - foreach ($value as &$node) { - switch ($type) { - case Database::VAR_DATETIME: - if (!($node instanceof UTCDateTime)) { - $node = new UTCDateTime(new \DateTime($node)); - } - break; - default: - break; - } - } - unset($node); - $document->setAttribute($key, ($array) ? $value : $value[0]); - } - - return $document; - } - - /** - * Create Documents in batches - * - * @param Document $collection - * @param array $documents - * - * @return array - * - * @throws DuplicateException - * @throws DatabaseException - */ - public function createDocuments(Document $collection, array $documents): array - { - $name = $this->getNamespace() . '_' . $this->filter($collection->getId()); - - $options = $this->getTransactionOptions(); - $records = []; - $hasSequence = null; - $documents = \array_map(fn ($doc) => clone $doc, $documents); - - foreach ($documents as $document) { - $sequence = $document->getSequence(); - - if ($hasSequence === null) { - $hasSequence = !empty($sequence); - } elseif ($hasSequence == empty($sequence)) { - throw new DatabaseException('All documents must have an sequence if one is set'); - } - - $record = $this->replaceChars('$', '_', (array)$document); - - if (!empty($sequence)) { - $record['_id'] = $sequence; - } - - $records[] = $record; - } - - try { - $documents = $this->client->insertMany($name, $records, $options); - } catch (MongoException $e) { - throw $this->processException($e); - } - - foreach ($documents as $index => $document) { - $documents[$index] = $this->replaceChars('_', '$', $this->client->toArray($document)); - $documents[$index] = new Document($documents[$index]); - } - - return $documents; - } - - /** - * - * @param string $name - * @param array $document - * @param array $options - * - * @return array - * @throws DuplicateException - * @throws Exception - */ - private function insertDocument(string $name, array $document, array $options = []): array - { - try { - $result = $this->client->insert($name, $document, $options); - $filters = []; - $filters['_uid'] = $document['_uid']; - - if ($this->sharedTables) { - $filters['_tenant'] = $this->getTenantFilters($name); - } - - try { - $result = $this->client->find( - $name, - $filters, - array_merge(['limit' => 1], $options) - )->cursor->firstBatch[0]; - } catch (MongoException $e) { - throw $this->processException($e); - } - - return $this->client->toArray($result); - } catch (MongoException $e) { - throw $this->processException($e); - } - } - - /** - * Update Document - * - * @param Document $collection - * @param string $id - * @param Document $document - * @param bool $skipPermissions - * @return Document - * @throws DuplicateException - * @throws DatabaseException - */ - public function updateDocument(Document $collection, string $id, Document $document, bool $skipPermissions): Document - { - $name = $this->getNamespace() . '_' . $this->filter($collection->getId()); - - $record = $document->getArrayCopy(); - $record = $this->replaceChars('$', '_', $record); - - $filters = []; - $filters['_uid'] = $id; - - if ($this->sharedTables) { - $filters['_tenant'] = $this->getTenantFilters($collection->getId()); - } - - try { - unset($record['_id']); // Don't update _id - - $options = $this->getTransactionOptions(); - $this->client->update($name, $filters, $record, $options); - } catch (MongoException $e) { - throw $this->processException($e); - } - - return $document; - } - - /** - * Update documents - * - * Updates all documents which match the given query. - * - * @param Document $collection - * @param Document $updates - * @param array $documents - * - * @return int - * - * @throws DatabaseException - */ - public function updateDocuments(Document $collection, Document $updates, array $documents): int - { - $name = $this->getNamespace() . '_' . $this->filter($collection->getId()); - - $options = $this->getTransactionOptions(); - $queries = [ - Query::equal('$sequence', \array_map(fn ($document) => $document->getSequence(), $documents)) - ]; - - $filters = $this->buildFilters($queries); - - if ($this->sharedTables) { - $filters['_tenant'] = $this->getTenantFilters($collection->getId()); - } - - $record = $updates->getArrayCopy(); - $record = $this->replaceChars('$', '_', $record); - - $updateQuery = [ - '$set' => $record, - ]; - - try { - return $this->client->update( - $name, - $filters, - $updateQuery, - options: $options, - multi: true, - ); - } catch (MongoException $e) { - throw $this->processException($e); - } - } - - /** - * @param Document $collection - * @param string $attribute - * @param array $changes - * @return array - * @throws DatabaseException - */ - public function upsertDocuments(Document $collection, string $attribute, array $changes): array - { - if (empty($changes)) { - return $changes; - } - - try { - $name = $this->getNamespace() . '_' . $this->filter($collection->getId()); - $attribute = $this->filter($attribute); - - $operations = []; - foreach ($changes as $change) { - $document = $change->getNew(); - $attributes = $document->getAttributes(); - $attributes['_uid'] = $document->getId(); - $attributes['_createdAt'] = $document['$createdAt']; - $attributes['_updatedAt'] = $document['$updatedAt']; - $attributes['_permissions'] = $document->getPermissions(); - - if (!empty($document->getSequence())) { - $attributes['_id'] = $document->getSequence(); - } - - if ($this->sharedTables) { - $attributes['_tenant'] = $document->getTenant(); - } - - $record = $this->replaceChars('$', '_', $attributes); - - // Build filter for upsert - $filters = ['_uid' => $document->getId()]; - - if ($this->sharedTables) { - $filters['_tenant'] = $this->getTenantFilters($collection->getId()); - } - - unset($record['_id']); // Don't update _id - - if (!empty($attribute)) { - // Get the attribute value before removing it from $set - $attributeValue = $record[$attribute] ?? 0; - - // Remove the attribute from $set since we're incrementing it - // it is requierd to mimic the behaver of SQL on duplicate key update - unset($record[$attribute]); - - // Increment the specific attribute and update all other fields - $update = [ - '$inc' => [$attribute => $attributeValue], - '$set' => $record - ]; - } else { - // Update all fields - $update = [ - '$set' => $record - ]; - - // Add UUID7 _id for new documents in upsert operations - if (empty($document->getSequence())) { - $update['$setOnInsert'] = [ - '_id' => $this->client->createUuid() - ]; - } - } - - $operations[] = [ - 'filter' => $filters, - 'update' => $update, - ]; - } - - $options = $this->getTransactionOptions(); - - $this->client->upsert( - $name, - $operations, - options: $options - ); - - } catch (MongoException $e) { - throw $this->processException($e); - } - - return \array_map(fn ($change) => $change->getNew(), $changes); - } - - /** - * Get sequences for documents that were created - * - * @param string $collection - * @param array $documents - * @return array - * @throws DatabaseException - * @throws MongoException - */ - public function getSequences(string $collection, array $documents): array - { - $documentIds = []; - $documentTenants = []; - foreach ($documents as $document) { - if (empty($document->getSequence())) { - $documentIds[] = $document->getId(); - - if ($this->sharedTables) { - $documentTenants[] = $document->getTenant(); - } - } - } - - if (empty($documentIds)) { - return $documents; - } - - $sequences = []; - $name = $this->getNamespace() . '_' . $this->filter($collection); - - $filters = ['_uid' => ['$in' => $documentIds]]; - - if ($this->sharedTables) { - $filters['_tenant'] = $this->getTenantFilters($collection, $documentTenants); - } - try { - // Use cursor paging for large result sets - $options = [ - 'projection' => ['_uid' => 1, '_id' => 1], - 'batchSize' => self::DEFAULT_BATCH_SIZE - ]; - - $options = $this->getTransactionOptions($options); - $response = $this->client->find($name, $filters, $options); - $results = $response->cursor->firstBatch ?? []; - - // Process first batch - foreach ($results as $result) { - $sequences[$result->_uid] = (string)$result->_id; - } - - // Get cursor ID for subsequent batches - $cursorId = $response->cursor->id ?? null; - - // Continue fetching with getMore - while ($cursorId && $cursorId !== 0) { - $moreResponse = $this->client->getMore((int)$cursorId, $name, self::DEFAULT_BATCH_SIZE); - $moreResults = $moreResponse->cursor->nextBatch ?? []; - - if (empty($moreResults)) { - break; - } - - foreach ($moreResults as $result) { - $sequences[$result->_uid] = (string)$result->_id; - } - - // Update cursor ID for next iteration - $cursorId = (int)($moreResponse->cursor->id ?? 0); - } - } catch (MongoException $e) { - throw $this->processException($e); - } - - foreach ($documents as $document) { - if (isset($sequences[$document->getId()])) { - $document['$sequence'] = $sequences[$document->getId()]; - } - } - - return $documents; - } - - /** - * Increase or decrease an attribute value - * - * @param string $collection - * @param string $id - * @param string $attribute - * @param int|float $value - * @param string $updatedAt - * @param int|float|null $min - * @param int|float|null $max - * @return bool - * @throws DatabaseException - * @throws MongoException - * @throws Exception - */ - public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value, string $updatedAt, int|float|null $min = null, int|float|null $max = null): bool - { - $attribute = $this->filter($attribute); - $filters = ['_uid' => $id]; - - if ($this->sharedTables) { - $filters['_tenant'] = $this->getTenantFilters($collection); - } - - if ($max) { - $filters[$attribute] = ['$lte' => $max]; - } - - if ($min) { - $filters[$attribute] = ['$gte' => $min]; - } - - $options = $this->getTransactionOptions(); - $this->client->update( - $this->getNamespace() . '_' . $this->filter($collection), - $filters, - [ - '$inc' => [$attribute => $value], - '$set' => ['_updatedAt' => $this->toMongoDatetime($updatedAt)], - ], - options: $options - ); - - return true; - } - - /** - * Delete Document - * - * @param string $collection - * @param string $id - * - * @return bool - * @throws Exception - */ - public function deleteDocument(string $collection, string $id): bool - { - $name = $this->getNamespace() . '_' . $this->filter($collection); - - $filters = []; - $filters['_uid'] = $id; - - if ($this->sharedTables) { - $filters['_tenant'] = $this->getTenantFilters($collection); - } - - $options = $this->getTransactionOptions(); - $result = $this->client->delete($name, $filters, 1, [], $options); - - return (!!$result); - } - - /** - * Delete Documents - * - * @param string $collection - * @param array $sequences - * @param array $permissionIds - * @return int - * @throws DatabaseException - */ - public function deleteDocuments(string $collection, array $sequences, array $permissionIds): int - { - $name = $this->getNamespace() . '_' . $this->filter($collection); - - foreach ($sequences as $index => $sequence) { - $sequences[$index] = $sequence; - } - - $filters = $this->buildFilters([new Query(Query::TYPE_EQUAL, '_id', $sequences)]); - - if ($this->sharedTables) { - $filters['_tenant'] = $this->getTenantFilters($collection); - } - - $filters = $this->replaceInternalIdsKeys($filters, '$', '_', $this->operators); - - $options = $this->getTransactionOptions(); - - try { - return $this->client->delete( - collection: $name, - filters: $filters, - limit: 0, - options: $options - ); - } catch (MongoException $e) { - throw $this->processException($e); - } - } - - /** - * Update Attribute. - * @param string $collection - * @param string $id - * @param string $type - * @param int $size - * @param bool $signed - * @param bool $array - * @param string $newKey - * - * @return bool - */ - public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool - { - if (!empty($newKey) && $newKey !== $id) { - return $this->renameAttribute($collection, $id, $newKey); - } - return true; - } - - /** - * TODO Consider moving this to adapter.php - * @param string $attribute - * @return string - */ - protected function getInternalKeyForAttribute(string $attribute): string - { - return match ($attribute) { - '$id' => '_uid', - '$sequence' => '_id', - '$collection' => '_collection', - '$tenant' => '_tenant', - '$createdAt' => '_createdAt', - '$updatedAt' => '_updatedAt', - '$permissions' => '_permissions', - default => $attribute - }; - } - - - /** - * Find Documents - * - * Find data sets using chosen queries - * - * @param Document $collection - * @param array $queries - * @param int|null $limit - * @param int|null $offset - * @param array $orderAttributes - * @param array $orderTypes - * @param array $cursor - * @param string $cursorDirection - * @param string $forPermission - * - * @return array - * @throws Exception - * @throws TimeoutException - */ - public function find(Document $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER, string $forPermission = Database::PERMISSION_READ): array - { - $name = $this->getNamespace() . '_' . $this->filter($collection->getId()); - $queries = array_map(fn ($query) => clone $query, $queries); - - $filters = $this->buildFilters($queries); - - if ($this->sharedTables) { - $filters['_tenant'] = $this->getTenantFilters($collection->getId()); - } - - // permissions - if (Authorization::$status) { - $roles = \implode('|', Authorization::getRoles()); - $filters['_permissions']['$in'] = [new Regex("{$forPermission}\\(\".*(?:{$roles}).*\"\\)", 'i')]; - } - - $options = []; - - if (!\is_null($limit)) { - $options['limit'] = $limit; - } - if (!\is_null($offset)) { - $options['skip'] = $offset; - } - - if ($this->timeout) { - $options['maxTimeMS'] = $this->timeout; - } - - $selections = $this->getAttributeSelections($queries); - if (!empty($selections) && !\in_array('*', $selections)) { - $options['projection'] = $this->getAttributeProjection($selections); - } - - // Add transaction context to options - $options = $this->getTransactionOptions($options); - - $orFilters = []; - - foreach ($orderAttributes as $i => $originalAttribute) { - $attribute = $this->getInternalKeyForAttribute($originalAttribute); - $attribute = $this->filter($attribute); - - $orderType = $this->filter($orderTypes[$i] ?? Database::ORDER_ASC); - $direction = $orderType; - - /** Get sort direction ASC || DESC **/ - if ($cursorDirection === Database::CURSOR_BEFORE) { - $direction = ($direction === Database::ORDER_ASC) - ? Database::ORDER_DESC - : Database::ORDER_ASC; - } - - $options['sort'][$attribute] = $this->getOrder($direction); - - /** Get operator sign '$lt' ? '$gt' **/ - $operator = $cursorDirection === Database::CURSOR_AFTER - ? ($orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER) - : ($orderType === Database::ORDER_DESC ? Query::TYPE_GREATER : Query::TYPE_LESSER); - - $operator = $this->getQueryOperator($operator); - - if (!empty($cursor)) { - - $andConditions = []; - for ($j = 0; $j < $i; $j++) { - $originalPrev = $orderAttributes[$j]; - $prevAttr = $this->filter($this->getInternalKeyForAttribute($originalPrev)); - $tmp = $cursor[$originalPrev]; - $andConditions[] = [ - $prevAttr => $tmp - ]; - } - - $tmp = $cursor[$originalAttribute]; - - if ($originalAttribute === '$sequence') { - /** If there is only $sequence attribute in $orderAttributes skip Or And operators **/ - if (count($orderAttributes) === 1) { - $filters[$attribute] = [ - $operator => $tmp - ]; - break; - } - } - - $andConditions[] = [ - $attribute => [ - $operator => $tmp - ] - ]; - - $orFilters[] = [ - '$and' => $andConditions - ]; - } - } - - if (!empty($orFilters)) { - $filters['$or'] = $orFilters; - } - - // Translate operators and handle time filters - $filters = $this->replaceInternalIdsKeys($filters, '$', '_', $this->operators); - - $found = []; - $cursorId = null; - - try { - // Use proper cursor iteration with reasonable batch size - $options['batchSize'] = self::DEFAULT_BATCH_SIZE; - - $response = $this->client->find($name, $filters, $options); - $results = $response->cursor->firstBatch ?? []; - // Process first batch - foreach ($results as $result) { - $record = $this->replaceChars('_', '$', (array)$result); - $found[] = new Document($record); - } - - // Get cursor ID for subsequent batches - $cursorId = $response->cursor->id ?? null; - - // Continue fetching with getMore - while ($cursorId && $cursorId !== 0) { - $moreResponse = $this->client->getMore((int)$cursorId, $name, self::DEFAULT_BATCH_SIZE); - $moreResults = $moreResponse->cursor->nextBatch ?? []; - - if (empty($moreResults)) { - break; - } - - foreach ($moreResults as $result) { - $record = $this->replaceChars('_', '$', (array)$result); - $found[] = new Document($record); - } - - $cursorId = (int)($moreResponse->cursor->id ?? 0); - } - - } catch (MongoException $e) { - throw $this->processException($e); - } finally { - // Ensure cursor is killed if still active to prevent resource leak - if (isset($cursorId) && $cursorId !== 0) { - try { - $this->client->query([ - 'killCursors' => $name, - 'cursors' => [(int)$cursorId] - ]); - } catch (\Exception $e) { - // Ignore errors during cursor cleanup - } - } - } - - if ($cursorDirection === Database::CURSOR_BEFORE) { - $found = array_reverse($found); - } - - return $found; - } - - - /** - * Converts Appwrite database type to MongoDB BSON type code. - * - * @param string $appwriteType - * @return string - */ - private function getMongoTypeCode(string $appwriteType): string - { - return match ($appwriteType) { - Database::VAR_STRING => 'string', - Database::VAR_INTEGER => 'int', - Database::VAR_FLOAT => 'double', - Database::VAR_BOOLEAN => 'bool', - Database::VAR_DATETIME => 'date', - Database::VAR_ID => 'string', - Database::VAR_UUID7 => 'string', - default => 'string' - }; - } - - /** - * Converts timestamp to Mongo\BSON datetime format. - * - * @param string $dt - * @return UTCDateTime - * @throws Exception - */ - private function toMongoDatetime(string $dt): UTCDateTime - { - return new UTCDateTime(new \DateTime($dt)); - } - - /** - * Recursive function to replace chars in array keys, while - * skipping any that are explicitly excluded. - * - * @param array $array - * @param string $from - * @param string $to - * @param array $exclude - * @return array - */ - private function replaceInternalIdsKeys(array $array, string $from, string $to, array $exclude = []): array - { - $result = []; - - foreach ($array as $key => $value) { - if (!in_array($key, $exclude)) { - $key = str_replace($from, $to, $key); - } - - $result[$key] = is_array($value) - ? $this->replaceInternalIdsKeys($value, $from, $to, $exclude) - : $value; - } - - return $result; - } - - - /** - * Count Documents - * - * @param Document $collection - * @param array $queries - * @param int|null $max - * @return int - * @throws Exception - */ - public function count(Document $collection, array $queries = [], ?int $max = null): int - { - $name = $this->getNamespace() . '_' . $this->filter($collection->getId()); - - $queries = array_map(fn ($query) => clone $query, $queries); - - $filters = []; - $options = []; - - if (!\is_null($max) && $max > 0) { - $options['limit'] = $max; - } - - if ($this->timeout) { - $options['maxTimeMS'] = $this->timeout; - } - - // Build filters from queries - $filters = $this->buildFilters($queries); - - if ($this->sharedTables) { - $filters['_tenant'] = $this->getTenantFilters($collection->getId()); - } - - // Add permissions filter if authorization is enabled - if (Authorization::$status) { - $roles = \implode('|', Authorization::getRoles()); - $filters['_permissions']['$in'] = [new Regex("read\\(\".*(?:{$roles}).*\"\\)", 'i')]; - } - - /** - * Use MongoDB aggregation pipeline for accurate counting - * Accuracy and Sharded Clusters - * "On a sharded cluster, the count command when run without a query predicate can result in an inaccurate - * count if orphaned documents exist or if a chunk migration is in progress. - * To avoid these situations, on a sharded cluster, use the db.collection.aggregate() method" - * https://www.mongodb.com/docs/manual/reference/command/count/#response - **/ - - $options = $this->getTransactionOptions(); - $pipeline = []; - - // Add match stage if filters are provided - if (!empty($filters)) { - $pipeline[] = ['$match' => $this->client->toObject($filters)]; - } - - // Add limit stage if specified - if (!\is_null($max) && $max > 0) { - $pipeline[] = ['$limit' => $max]; - } - - // Use $group and $sum when limit is specified, $count when no limit - // Note: $count stage doesn't works well with $limit in the same pipeline - // When limit is specified, we need to use $group + $sum to count the limited documents - if (!\is_null($max) && $max > 0) { - // When limit is specified, use $group and $sum to count limited documents - $pipeline[] = [ - '$group' => [ - '_id' => null, - 'total' => ['$sum' => 1]] - ]; - } else { - // When no limit is passed, use $count for better performance - $pipeline[] = [ - '$count' => 'total' - ]; - } - - try { - - $result = $this->client->aggregate($name, $pipeline, $options); - - // Aggregation returns stdClass with cursor property containing firstBatch - if (isset($result->cursor) && !empty($result->cursor->firstBatch)) { - $firstResult = $result->cursor->firstBatch[0]; - - // Handle both $count and $group response formats - if (isset($firstResult->total)) { - return (int)$firstResult->total; - } - } - - return 0; - } catch (MongoException $e) { - return 0; - } - } - - - /** - * Sum an attribute - * - * @param Document $collection - * @param string $attribute - * @param array $queries - * @param int|null $max - * - * @return int|float - * @throws Exception - */ - - public function sum(Document $collection, string $attribute, array $queries = [], ?int $max = null): float|int - { - $name = $this->getNamespace() . '_' . $this->filter($collection->getId()); - - // queries - $queries = array_map(fn ($query) => clone $query, $queries); - $filters = $this->buildFilters($queries); - - if ($this->sharedTables) { - $filters['_tenant'] = $this->getTenantFilters($collection->getId()); - } - - // permissions - if (Authorization::$status) { // skip if authorization is disabled - $roles = \implode('|', Authorization::getRoles()); - $filters['_permissions']['$in'] = [new Regex("read\\(\".*(?:{$roles}).*\"\\)", 'i')]; - } - - // using aggregation to get sum an attribute as described in - // https://docs.mongodb.com/manual/reference/method/db.collection.aggregate/ - // Pipeline consists of stages to aggregation, so first we set $match - // that will load only documents that matches the filters provided and passes to the next stage - // then we set $limit (if $max is provided) so that only $max documents will be passed to the next stage - // finally we use $group stage to sum the provided attribute that matches the given filters and max - // We pass the $pipeline to the aggregate method, which returns a cursor, then we get - // the array of results from the cursor, and we return the total sum of the attribute - $pipeline = []; - if (!empty($filters)) { - $pipeline[] = ['$match' => $filters]; - } - if (!empty($max)) { - $pipeline[] = ['$limit' => $max]; - } - $pipeline[] = [ - '$group' => [ - '_id' => null, - 'total' => ['$sum' => '$' . $attribute], - ], - ]; - - $options = $this->getTransactionOptions(); - return $this->client->aggregate($name, $pipeline, $options)->cursor->firstBatch[0]->total ?? 0; - } - - /** - * @return Client - * - * @throws Exception - */ - protected function getClient(): Client - { - return $this->client; - } - - /** - * Keys cannot begin with $ in MongoDB - * Convert $ prefix to _ on $id, $permissions, and $collection - * - * @param string $from - * @param string $to - * @param array $array - * @return array - */ - protected function replaceChars(string $from, string $to, array $array): array - { - $filter = [ - 'permissions', - 'createdAt', - 'updatedAt', - 'collection' - ]; - - // First pass: recursively process array values and collect keys to rename - $keysToRename = []; - foreach ($array as $k => $v) { - if (is_array($v)) { - $array[$k] = $this->replaceChars($from, $to, $v); - } - - // Handle key replacement for filtered attributes - $clean_key = str_replace($from, "", $k); - if (in_array($clean_key, $filter)) { - $newKey = str_replace($from, $to, $k); - if ($newKey !== $k) { - $keysToRename[$k] = $newKey; - } - } - } - - foreach ($keysToRename as $oldKey => $newKey) { - $array[$newKey] = $array[$oldKey]; - unset($array[$oldKey]); - } - - // Handle special attribute mappings - if ($from === '_') { - if (isset($array['_id'])) { - $array['$sequence'] = (string)$array['_id']; - unset($array['_id']); - } - if (isset($array['_uid'])) { - $array['$id'] = $array['_uid']; - unset($array['_uid']); - } - if (isset($array['_tenant'])) { - $array['$tenant'] = $array['_tenant']; - unset($array['_tenant']); - } - } elseif ($from === '$') { - if (isset($array['$id'])) { - $array['_uid'] = $array['$id']; - unset($array['$id']); - } - if (isset($array['$sequence'])) { - $array['_id'] = $array['$sequence']; - unset($array['$sequence']); - } - if (isset($array['$tenant'])) { - $array['_tenant'] = $array['$tenant']; - unset($array['$tenant']); - } - } - - return $array; - } - - /** - * @param array $queries - * @param string $separator - * @return array - * @throws Exception - */ - protected function buildFilters(array $queries, string $separator = '$and'): array - { - $filters = []; - $queries = Query::groupByType($queries)['filters']; - - foreach ($queries as $query) { - /* @var $query Query */ - if ($query->isNested()) { - $operator = $this->getQueryOperator($query->getMethod()); - - $filters[$separator][] = $this->buildFilters($query->getValues(), $operator); - } else { - $filters[$separator][] = $this->buildFilter($query); - } - } - - return $filters; - } - - /** - * @param Query $query - * @return array - * @throws Exception - */ - protected function buildFilter(Query $query): array - { - if ($query->getAttribute() === '$id') { - $query->setAttribute('_uid'); - } elseif ($query->getAttribute() === '$sequence') { - $query->setAttribute('_id'); - $values = $query->getValues(); - foreach ($values as $k => $v) { - $values[$k] = $v; - } - $query->setValues($values); - } elseif ($query->getAttribute() === '$createdAt') { - $query->setAttribute('_createdAt'); - } elseif ($query->getAttribute() === '$updatedAt') { - $query->setAttribute('_updatedAt'); - } - - $attribute = $query->getAttribute(); - $operator = $this->getQueryOperator($query->getMethod()); - - $value = match ($query->getMethod()) { - Query::TYPE_IS_NULL, - Query::TYPE_IS_NOT_NULL => null, - default => $this->getQueryValue( - $query->getMethod(), - count($query->getValues()) > 1 - ? $query->getValues() - : $query->getValues()[0] - ), - }; - - $filter = []; - - if ($operator == '$eq' && \is_array($value)) { - $filter[$attribute]['$in'] = $value; - } elseif ($operator == '$ne' && \is_array($value)) { - $filter[$attribute]['$nin'] = $value; - } elseif ($operator == '$in') { - if ($query->getMethod() === Query::TYPE_CONTAINS && !$query->onArray()) { - $filter[$attribute]['$regex'] = $this->createSafeRegex($value, '.*%s.*'); - } else { - $filter[$attribute]['$in'] = $query->getValues(); - } - } elseif ($operator === 'notContains') { - if (!$query->onArray()) { - $filter[$attribute] = ['$not' => $this->createSafeRegex($value, '.*%s.*')]; - } else { - $filter[$attribute]['$nin'] = $query->getValues(); - } - } elseif ($operator == '$search') { - if ($query->getMethod() === Query::TYPE_NOT_SEARCH) { - // MongoDB doesn't support negating $text expressions directly - // Use regex as fallback for NOT search while keeping fulltext for positive search - if (empty($value)) { - // If value is not passed, don't add any filter - this will match all documents - } else { - $filter[$attribute] = ['$not' => $this->createSafeRegex($value, '.*%s.*')]; - } - } else { - $filter['$text'][$operator] = $value; - } - } elseif ($operator === Query::TYPE_BETWEEN) { - $filter[$attribute]['$lte'] = $value[1]; - $filter[$attribute]['$gte'] = $value[0]; - } elseif ($operator === Query::TYPE_NOT_BETWEEN) { - $filter['$or'] = [ - [$attribute => ['$lt' => $value[0]]], - [$attribute => ['$gt' => $value[1]]] - ]; - } elseif ($operator === '$regex' && $query->getMethod() === Query::TYPE_NOT_STARTS_WITH) { - $filter[$attribute] = ['$not' => $this->createSafeRegex($value, '^%s')]; - } elseif ($operator === '$regex' && $query->getMethod() === Query::TYPE_NOT_ENDS_WITH) { - $filter[$attribute] = ['$not' => $this->createSafeRegex($value, '%s$')]; - } else { - $filter[$attribute][$operator] = $value; - } - - return $filter; - } - - /** - * Get Query Operator - * - * @param string $operator - * - * @return string - * @throws Exception - */ - protected function getQueryOperator(string $operator): string - { - return match ($operator) { - Query::TYPE_EQUAL, - Query::TYPE_IS_NULL => '$eq', - Query::TYPE_NOT_EQUAL, - Query::TYPE_IS_NOT_NULL => '$ne', - Query::TYPE_LESSER => '$lt', - Query::TYPE_LESSER_EQUAL => '$lte', - Query::TYPE_GREATER => '$gt', - Query::TYPE_GREATER_EQUAL => '$gte', - Query::TYPE_CONTAINS => '$in', - Query::TYPE_NOT_CONTAINS => 'notContains', - Query::TYPE_SEARCH => '$search', - Query::TYPE_NOT_SEARCH => '$search', - Query::TYPE_BETWEEN => 'between', - Query::TYPE_NOT_BETWEEN => 'notBetween', - Query::TYPE_STARTS_WITH, - Query::TYPE_NOT_STARTS_WITH, - Query::TYPE_ENDS_WITH, - Query::TYPE_NOT_ENDS_WITH => '$regex', - Query::TYPE_OR => '$or', - Query::TYPE_AND => '$and', - default => throw new DatabaseException('Unknown operator:' . $operator . '. Must be one of ' . Query::TYPE_EQUAL . ', ' . Query::TYPE_NOT_EQUAL . ', ' . Query::TYPE_LESSER . ', ' . Query::TYPE_LESSER_EQUAL . ', ' . Query::TYPE_GREATER . ', ' . Query::TYPE_GREATER_EQUAL . ', ' . Query::TYPE_IS_NULL . ', ' . Query::TYPE_IS_NOT_NULL . ', ' . Query::TYPE_BETWEEN . ', ' . Query::TYPE_NOT_BETWEEN . ', ' . Query::TYPE_STARTS_WITH . ', ' . Query::TYPE_NOT_STARTS_WITH . ', ' . Query::TYPE_ENDS_WITH . ', ' . Query::TYPE_NOT_ENDS_WITH . ', ' . Query::TYPE_CONTAINS . ', ' . Query::TYPE_NOT_CONTAINS . ', ' . Query::TYPE_SEARCH . ', ' . Query::TYPE_NOT_SEARCH . ', ' . Query::TYPE_SELECT), - }; - } - - protected function getQueryValue(string $method, mixed $value): mixed - { - switch ($method) { - case Query::TYPE_STARTS_WITH: - $value = preg_quote($value, '/'); - $value = str_replace(['\\', '$'], ['\\\\', '\\$'], $value); - return $value . '.*'; - case Query::TYPE_NOT_STARTS_WITH: - return $value; - case Query::TYPE_ENDS_WITH: - $value = preg_quote($value, '/'); - $value = str_replace(['\\', '$'], ['\\\\', '\\$'], $value); - return '.*' . $value; - case Query::TYPE_NOT_ENDS_WITH: - return $value; - default: - return $value; - } - } - - /** - * Get Mongo Order - * - * @param string $order - * - * @return int - * @throws Exception - */ - protected function getOrder(string $order): int - { - return match ($order) { - Database::ORDER_ASC => 1, - Database::ORDER_DESC => -1, - default => throw new DatabaseException('Unknown sort order:' . $order . '. Must be one of ' . Database::ORDER_ASC . ', ' . Database::ORDER_DESC), - }; - } - - /** - * @param array $selections - * @param string $prefix - * @return mixed - */ - protected function getAttributeProjection(array $selections, string $prefix = ''): mixed - { - $projection = []; - - $internalKeys = \array_map( - fn ($attr) => $attr['$id'], - Database::INTERNAL_ATTRIBUTES - ); - - foreach ($selections as $selection) { - // Skip internal attributes since all are selected by default - if (\in_array($selection, $internalKeys)) { - continue; - } - - $projection[$selection] = 1; - } - - $projection['_uid'] = 1; - $projection['_id'] = 1; - $projection['_createdAt'] = 1; - $projection['_updatedAt'] = 1; - $projection['_permissions'] = 1; - - return $projection; - } - - /** - * Get max STRING limit - * - * @return int - */ - public function getLimitForString(): int - { - return 2147483647; - } - - /** - * Get max INT limit - * - * @return int - */ - public function getLimitForInt(): int - { - // Mongo does not handle integers directly, so using MariaDB limit for now - return 4294967295; - } - - /** - * Get maximum column limit. - * Returns 0 to indicate no limit - * - * @return int - */ - public function getLimitForAttributes(): int - { - return 0; - } - - /** - * Get maximum index limit. - * https://docs.mongodb.com/manual/reference/limits/#mongodb-limit-Number-of-Indexes-per-Collection - * - * @return int - */ - public function getLimitForIndexes(): int - { - return 64; - } - - public function getMinDateTime(): \DateTime - { - return new \DateTime('-9999-01-01 00:00:00'); - } - - /** - * Is schemas supported? - * - * @return bool - */ - public function getSupportForSchemas(): bool - { - return false; - } - - /** - * Is index supported? - * - * @return bool - */ - public function getSupportForIndex(): bool - { - return true; - } - - public function getSupportForIndexArray(): bool - { - return true; - } - - /** - * Is internal casting supported? - * - * @return bool - */ - public function getSupportForInternalCasting(): bool - { - return true; - } - - public function getSupportForUTCCasting(): bool - { - return true; - } - - public function setUTCDatetime(string $value): mixed - { - return new UTCDateTime(new \DateTime($value)); - } - - - /** - * Are attributes supported? - * - * @return bool - */ - public function getSupportForAttributes(): bool - { - return true; - } - - /** - * Is unique index supported? - * - * @return bool - */ - public function getSupportForUniqueIndex(): bool - { - return true; - } - - /** - * Is fulltext index supported? - * - * @return bool - */ - public function getSupportForFulltextIndex(): bool - { - return true; - } - - /** - * Is fulltext Wildcard index supported? - * - * @return bool - */ - public function getSupportForFulltextWildcardIndex(): bool - { - return false; - } - - /** - * Does the adapter handle Query Array Contains? - * - * @return bool - */ - public function getSupportForQueryContains(): bool - { - return false; - } - - /** - * Are timeouts supported? - * - * @return bool - */ - public function getSupportForTimeouts(): bool - { - return true; - } - - public function getSupportForRelationships(): bool - { - return false; - } - - public function getSupportForUpdateLock(): bool - { - return false; - } - - public function getSupportForAttributeResizing(): bool - { - return false; - } - - /** - * Are batch operations supported? - * - * @return bool - */ - public function getSupportForBatchOperations(): bool - { - return false; - } - - /** - * Is get connection id supported? - * - * @return bool - */ - public function getSupportForGetConnectionId(): bool - { - return false; - } - - /** - * Is cache fallback supported? - * - * @return bool - */ - public function getSupportForCacheSkipOnFailure(): bool - { - return false; - } - - /** - * Is hostname supported? - * - * @return bool - */ - public function getSupportForHostname(): bool - { - return true; - } - - /** - * Is get schema attributes supported? - * - * @return bool - */ - public function getSupportForSchemaAttributes(): bool - { - return false; - } - - public function getSupportForCastIndexArray(): bool - { - return false; - } - - public function getSupportForUpserts(): bool - { - return true; - } - - public function getSupportForReconnection(): bool - { - return false; - } - - public function getSupportForBatchCreateAttributes(): bool - { - return true; - } - - /** - * Get current attribute count from collection document - * - * @param Document $collection - * @return int - */ - public function getCountOfAttributes(Document $collection): int - { - $attributes = \count($collection->getAttribute('attributes') ?? []); - - return $attributes + static::getCountOfDefaultAttributes(); - } - - /** - * Get current index count from collection document - * - * @param Document $collection - * @return int - */ - public function getCountOfIndexes(Document $collection): int - { - $indexes = \count($collection->getAttribute('indexes') ?? []); - - return $indexes + static::getCountOfDefaultIndexes(); - } - - /** - * Returns number of attributes used by default. - *p - * @return int - */ - public function getCountOfDefaultAttributes(): int - { - return \count(Database::INTERNAL_ATTRIBUTES); - } - - /** - * Returns number of indexes used by default. - * - * @return int - */ - public function getCountOfDefaultIndexes(): int - { - return \count(Database::INTERNAL_INDEXES); - } - - /** - * Get maximum width, in bytes, allowed for a SQL row - * Return 0 when no restrictions apply - * - * @return int - */ - public function getDocumentSizeLimit(): int - { - return 0; - } - - /** - * Estimate maximum number of bytes required to store a document in $collection. - * Byte requirement varies based on column type and size. - * Needed to satisfy MariaDB/MySQL row width limit. - * Return 0 when no restrictions apply to row width - * - * @param Document $collection - * @return int - */ - public function getAttributeWidth(Document $collection): int - { - return 0; - } - - /** - * Is casting supported? - * - * @return bool - */ - public function getSupportForCasting(): bool - { - return true; - } - - /** - * Is spatial attributes supported? - * - * @return bool - */ - public function getSupportForSpatialAttributes(): bool - { - return false; - } - - /** - * Get Support for Null Values in Spatial Indexes - * - * @return bool - */ - public function getSupportForSpatialIndexNull(): bool - { - return false; - } - - /** - * Does the adapter includes boundary during spatial contains? - * - * @return bool - */ - - public function getSupportForBoundaryInclusiveContains(): bool - { - return false; - } - - /** - * Does the adapter support order attribute in spatial indexes? - * - * @return bool - */ - public function getSupportForSpatialIndexOrder(): bool - { - return false; - } - - - /** - * Does the adapter support spatial axis order specification? - * - * @return bool - */ - public function getSupportForSpatialAxisOrder(): bool - { - return false; - } - - /** - * Does the adapter support calculating distance(in meters) between multidimension geometry(line, polygon,etc)? - * - * @return bool - */ - public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool - { - return false; - } - - public function getSupportForOptionalSpatialAttributeWithExistingRows(): bool - { - return false; - } - - /** - * Does the adapter support multiple fulltext indexes? - * - * @return bool - */ - public function getSupportForMultipleFulltextIndexes(): bool - { - return false; - } - - /** - * Does the adapter support identical indexes? - * - * @return bool - */ - public function getSupportForIdenticalIndexes(): bool - { - return false; - } - - /** - * Does the adapter support random order for queries? - * - * @return bool - */ - public function getSupportForOrderRandom(): bool - { - return false; - } - - /** - * Flattens the array. - * - * @param mixed $list - * @return array - */ - protected function flattenArray(mixed $list): array - { - if (!is_array($list)) { - // make sure the input is an array - return array($list); - } - - $newArray = []; - - foreach ($list as $value) { - $newArray = array_merge($newArray, $this->flattenArray($value)); - } - - return $newArray; - } - - /** - * @param array|Document $target - * @return array - */ - protected function removeNullKeys(array|Document $target): array - { - $target = \is_array($target) ? $target : $target->getArrayCopy(); - $cleaned = []; - - foreach ($target as $key => $value) { - if (\is_null($value)) { - continue; - } - - $cleaned[$key] = $value; - } - - - return $cleaned; - } - - public function getKeywords(): array - { - return []; - } - - protected function processException(Exception $e): \Exception - { - // Timeout - if ($e->getCode() === 50) { - return new TimeoutException('Query timed out', $e->getCode(), $e); - } - - // Duplicate key error - if ($e->getCode() === 11000) { - return new DuplicateException('Document already exists', $e->getCode(), $e); - } - - // Duplicate key error for unique index - if ($e->getCode() === 11001) { - return new DuplicateException('Document already exists', $e->getCode(), $e); - } - - // Collection already exists - if ($e->getCode() === 48) { - return new DuplicateException('Collection already exists', $e->getCode(), $e); - } - - // Index already exists - if ($e->getCode() === 85) { - return new DuplicateException('Index already exists', $e->getCode(), $e); - } - - // No transaction - if ($e->getCode() === 251) { - return new TransactionException('No active transaction', $e->getCode(), $e); - } - - return $e; - } - - protected function quote(string $string): string - { - return ""; - } - - /** - * @param mixed $stmt - * @return bool - */ - protected function execute(mixed $stmt): bool - { - return true; - } - - /** - * @return string - */ - public function getIdAttributeType(): string - { - return Database::VAR_UUID7; - } - - /** - * @return int - */ - public function getMaxIndexLength(): int - { - return 1024; - } - - public function getConnectionId(): string - { - return '0'; - } - - public function getInternalIndexesKeys(): array - { - return []; - } - - public function getSchemaAttributes(string $collection): array - { - return []; - } - - /** - * @param string $collection - * @param array $tenants - * @return int|null|array> - */ - public function getTenantFilters( - string $collection, - array $tenants = [], - ): int|null|array { - $values = []; - if (!$this->sharedTables) { - return $values; - } - - if (\count($tenants) === 0) { - $values[] = $this->getTenant(); - } else { - for ($index = 0; $index < \count($tenants); $index++) { - $values[] = $tenants[$index]; - } - } - - if ($collection === Database::METADATA) { - $values[] = null; - } - - if (\count($values) === 1) { - return $values[0]; - } - - - return ['$in' => $values]; - } - - public function decodePoint(string $wkb): array - { - return []; - } - - /** - * Decode a WKB or textual LINESTRING into [[x1, y1], [x2, y2], ...] - * - * @param string $wkb - * @return float[][] Array of points, each as [x, y] - */ - public function decodeLinestring(string $wkb): array - { - return []; - } - - /** - * Decode a WKB or textual POLYGON into [[[x1, y1], [x2, y2], ...], ...] - * - * @param string $wkb - * @return float[][][] Array of rings, each ring is an array of points [x, y] - */ - public function decodePolygon(string $wkb): array - { - return []; - } - - /** - * Get the query to check for tenant when in shared tables mode - * - * @param string $collection The collection being queried - * @param string $alias The alias of the parent collection if in a subquery - * @return string - */ - public function getTenantQuery(string $collection, string $alias = ''): string - { - return ''; - } -} diff --git a/src/Database/Adapter/Pool.php b/src/Database/Adapter/Pool.php index f89c09873..799596d2d 100644 --- a/src/Database/Adapter/Pool.php +++ b/src/Database/Adapter/Pool.php @@ -470,7 +470,7 @@ public function getKeywords(): array return $this->delegate(__FUNCTION__, \func_get_args()); } - protected function getAttributeProjection(array $selections, string $prefix): mixed + protected function getAttributeProjection(array $selections, string $prefix): string { return $this->delegate(__FUNCTION__, \func_get_args()); } @@ -524,37 +524,36 @@ public function getSupportForSpatialIndexOrder(): bool { return $this->delegate(__FUNCTION__, \func_get_args()); } - + /** + * Does the adapter support calculating distance(in meters) between multidimension geometry(line, polygon,etc)? + * + * @return bool + */ public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool { return $this->delegate(__FUNCTION__, \func_get_args()); } + /** + * Does the adapter support spatial axis order specification? + * + * @return bool + */ public function getSupportForSpatialAxisOrder(): bool { return $this->delegate(__FUNCTION__, \func_get_args()); } + /** + * Adapter supports optional spatial attributes with existing rows. + * + * @return bool + */ public function getSupportForOptionalSpatialAttributeWithExistingRows(): bool { return $this->delegate(__FUNCTION__, \func_get_args()); } - public function getSupportForMultipleFulltextIndexes(): bool - { - return $this->delegate(__FUNCTION__, \func_get_args()); - } - - public function getSupportForIdenticalIndexes(): bool - { - return $this->delegate(__FUNCTION__, \func_get_args()); - } - - public function getSupportForOrderRandom(): bool - { - return $this->delegate(__FUNCTION__, \func_get_args()); - } - public function decodePoint(string $wkb): array { return $this->delegate(__FUNCTION__, \func_get_args()); @@ -569,29 +568,4 @@ public function decodePolygon(string $wkb): array { return $this->delegate(__FUNCTION__, \func_get_args()); } - - public function castingBefore(Document $collection, Document $document): Document - { - return $this->delegate(__FUNCTION__, \func_get_args()); - } - - public function castingAfter(Document $collection, Document $document): Document - { - return $this->delegate(__FUNCTION__, \func_get_args()); - } - - public function getSupportForInternalCasting(): bool - { - return $this->delegate(__FUNCTION__, \func_get_args()); - } - - public function getSupportForUTCCasting(): bool - { - return $this->delegate(__FUNCTION__, \func_get_args()); - } - - public function setUTCDatetime(string $value): mixed - { - return $this->delegate(__FUNCTION__, \func_get_args()); - } } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 395c8293a..84fae6ce7 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -1512,66 +1512,6 @@ public function getSupportForSpatialIndexOrder(): bool return false; } - /** - * Is internal casting supported? - * - * @return bool - */ - public function getSupportForInternalCasting(): bool - { - return false; - } - - /** - * Does the adapter support multiple fulltext indexes? - * - * @return bool - */ - public function getSupportForMultipleFulltextIndexes(): bool - { - return true; - } - - /** - * Does the adapter support identical indexes? - * - * @return bool - */ - public function getSupportForIdenticalIndexes(): bool - { - return true; - } - - /** - * Does the adapter support random order for queries? - * - * @return bool - */ - public function getSupportForOrderRandom(): bool - { - return true; - } - - public function getSupportForUTCCasting(): bool - { - return false; - } - - public function setUTCDatetime(string $value): mixed - { - return $value; - } - - public function castingBefore(Document $collection, Document $document): Document - { - return $document; - } - - public function castingAfter(Document $collection, Document $document): Document - { - return $document; - } - /** * Does the adapter support spatial axis order specification? * @@ -1941,10 +1881,10 @@ public function getTenantQuery( * * @param array $selections * @param string $prefix - * @return mixed + * @return string * @throws Exception */ - protected function getAttributeProjection(array $selections, string $prefix): mixed + protected function getAttributeProjection(array $selections, string $prefix): string { if (empty($selections) || \in_array('*', $selections)) { return "{$this->quote($prefix)}.*"; diff --git a/src/Database/Database.php b/src/Database/Database.php index 8bea1cb75..14e04844c 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -43,7 +43,7 @@ class Database public const VAR_BOOLEAN = 'boolean'; public const VAR_DATETIME = 'datetime'; public const VAR_ID = 'id'; - public const VAR_UUID7 = 'uuid7'; + public const VAR_OBJECT_ID = 'objectId'; public const INT_MAX = 2147483647; public const BIG_INT_MAX = PHP_INT_MAX; @@ -1405,16 +1405,12 @@ public function createCollection(string $id, array $attributes = [], array $inde if ($this->validate) { $validator = new IndexValidator( $attributes, - [], $this->adapter->getMaxIndexLength(), $this->adapter->getInternalIndexesKeys(), $this->adapter->getSupportForIndexArray(), $this->adapter->getSupportForSpatialAttributes(), $this->adapter->getSupportForSpatialIndexNull(), $this->adapter->getSupportForSpatialIndexOrder(), - $this->adapter->getSupportForAttributes(), - $this->adapter->getSupportForMultipleFulltextIndexes(), - $this->adapter->getSupportForIdenticalIndexes(), ); foreach ($indexes as $index) { if (!$validator->isValid($index)) { @@ -2248,13 +2244,6 @@ public function updateAttributeDefault(string $collection, string $id, mixed $de public function updateAttribute(string $collection, string $id, ?string $type = null, ?int $size = null, ?bool $required = null, mixed $default = null, ?bool $signed = null, ?bool $array = null, ?string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document { return $this->updateAttributeMeta($collection, $id, function ($attribute, $collectionDoc, $attributeIndex) use ($collection, $id, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters, $newKey) { - - // Store original indexes before any modifications (deep copy preserving Document objects) - $originalIndexes = []; - foreach ($collectionDoc->getAttribute('indexes', []) as $index) { - $originalIndexes[] = clone $index; - } - $altering = !\is_null($type) || !\is_null($size) || !\is_null($signed) @@ -2425,16 +2414,12 @@ public function updateAttribute(string $collection, string $id, ?string $type = if ($this->validate) { $validator = new IndexValidator( $attributes, - $originalIndexes, $this->adapter->getMaxIndexLength(), $this->adapter->getInternalIndexesKeys(), $this->adapter->getSupportForIndexArray(), $this->adapter->getSupportForSpatialAttributes(), $this->adapter->getSupportForSpatialIndexNull(), $this->adapter->getSupportForSpatialIndexOrder(), - $this->adapter->getSupportForAttributes(), - $this->adapter->getSupportForMultipleFulltextIndexes(), - $this->adapter->getSupportForIdenticalIndexes(), ); foreach ($indexes as $index) { @@ -3367,28 +3352,23 @@ public function createIndex(string $collection, string $id, string $type, array 'orders' => $orders, ]); - if ($this->validate) { + $collection->setAttribute('indexes', $index, Document::SET_TYPE_APPEND); + if ($this->validate) { $validator = new IndexValidator( $collection->getAttribute('attributes', []), - $collection->getAttribute('indexes', []), $this->adapter->getMaxIndexLength(), $this->adapter->getInternalIndexesKeys(), $this->adapter->getSupportForIndexArray(), $this->adapter->getSupportForSpatialAttributes(), $this->adapter->getSupportForSpatialIndexNull(), $this->adapter->getSupportForSpatialIndexOrder(), - $this->adapter->getSupportForAttributes(), - $this->adapter->getSupportForMultipleFulltextIndexes(), - $this->adapter->getSupportForIdenticalIndexes(), ); if (!$validator->isValid($index)) { throw new IndexException($validator->getDescription()); } } - $collection->setAttribute('indexes', $index, Document::SET_TYPE_APPEND); - try { $created = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes, $lengths, $orders, $indexAttributesWithTypes); @@ -3545,9 +3525,6 @@ public function getDocument(string $collection, string $id, array $queries = [], if ($document->isEmpty()) { return $document; } - - $document = $this->adapter->castingAfter($collection, $document); - $document->setAttribute('$collection', $collection->getId()); if ($collection->getId() !== self::METADATA) { @@ -4265,14 +4242,11 @@ public function createDocument(string $collection, Document $document): Document $this->adapter->getIdAttributeType(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() ); if (!$structure->isValid($document)) { throw new StructureException($structure->getDescription()); } - $document = $this->adapter->castingBefore($collection, $document); - $document = $this->withTransaction(function () use ($collection, $document) { if ($this->resolveRelationships) { $document = $this->silent(fn () => $this->createDocumentRelationships($collection, $document)); @@ -4284,7 +4258,7 @@ public function createDocument(string $collection, Document $document): Document // Use the write stack depth for proper MAX_DEPTH enforcement during creation $fetchDepth = count($this->relationshipWriteStack); $documents = $this->silent(fn () => $this->populateDocumentsRelationships([$document], $collection, $fetchDepth)); - $document = $this->adapter->castingAfter($collection, $documents[0]); + $document = $documents[0]; } $document = $this->casting($collection, $document); @@ -4367,7 +4341,6 @@ public function createDocuments( $this->adapter->getIdAttributeType(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() ); if (!$validator->isValid($document)) { throw new StructureException($validator->getDescription()); @@ -4376,8 +4349,6 @@ public function createDocuments( if ($this->resolveRelationships) { $document = $this->silent(fn () => $this->createDocumentRelationships($collection, $document)); } - - $document = $this->adapter->castingBefore($collection, $document); } foreach (\array_chunk($documents, $batchSize) as $chunk) { @@ -4392,7 +4363,6 @@ public function createDocuments( } foreach ($batch as $document) { - $document = $this->adapter->castingAfter($collection, $document); $document = $this->casting($collection, $document); $document = $this->decode($collection, $document); @@ -4921,7 +4891,6 @@ public function updateDocument(string $collection, string $id, Document $documen $this->adapter->getIdAttributeType(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() ); if (!$structureValidator->isValid($document)) { // Make sure updated structure still apply collection rules (if any) throw new StructureException($structureValidator->getDescription()); @@ -4931,13 +4900,7 @@ public function updateDocument(string $collection, string $id, Document $documen $document = $this->silent(fn () => $this->updateDocumentRelationships($collection, $old, $document)); } - - $document = $this->adapter->castingBefore($collection, $document); - $this->adapter->updateDocument($collection, $id, $document, $skipPermissionsUpdate); - - $document = $this->adapter->castingAfter($collection, $document); - $this->purgeCachedDocument($collection->getId(), $id); return $document; @@ -5057,7 +5020,6 @@ public function updateDocuments( $this->adapter->getIdAttributeType(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() ); if (!$validator->isValid($updates)) { @@ -5132,8 +5094,7 @@ public function updateDocuments( if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { throw new ConflictException('Document was updated after the request timestamp'); } - $encoded = $this->encode($collection, $document); - $batch[$index] = $this->adapter->castingBefore($collection, $encoded); + $batch[$index] = $this->encode($collection, $document); } $this->adapter->updateDocuments( @@ -5143,11 +5104,7 @@ public function updateDocuments( ); }); - $updates = $this->adapter->castingBefore($collection, $updates); - - foreach ($batch as $index => $doc) { - $doc = $this->adapter->castingAfter($collection, $doc); $doc->removeAttribute('$skipPermissionsUpdate'); $this->purgeCachedDocument($collection->getId(), $doc->getId()); $doc = $this->decode($collection, $doc); @@ -5769,7 +5726,6 @@ public function upsertDocumentsWithIncrease( $this->adapter->getIdAttributeType(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() ); if (!$validator->isValid($document)) { @@ -5795,9 +5751,6 @@ public function upsertDocumentsWithIncrease( $seenIds[] = $document->getId(); - $old = $this->adapter->castingBefore($collection, $old); - $document = $this->adapter->castingBefore($collection, $document); - $documents[$key] = new Change( old: $old, new: $document @@ -5834,7 +5787,6 @@ public function upsertDocumentsWithIncrease( } foreach ($batch as $index => $doc) { - $doc = $this->adapter->castingAfter($collection, $doc); $doc = $this->decode($collection, $doc); if ($this->getSharedTables() && $this->getTenantPerDocument()) { @@ -6761,7 +6713,6 @@ public function find(string $collection, array $queries = [], string $forPermiss $this->maxQueryValues, $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); @@ -6817,13 +6768,7 @@ public function find(string $collection, array $queries = [], string $forPermiss throw new DatabaseException("cursor Document must be from the same Collection."); } - if (!empty($cursor)) { - $cursor = $this->encode($collection, $cursor); - $cursor = $this->adapter->castingBefore($collection, $cursor); - $cursor = $cursor->getArrayCopy(); - } else { - $cursor = []; - } + $cursor = empty($cursor) ? [] : $this->encode($collection, $cursor)->getArrayCopy(); /** @var array $queries */ $queries = \array_merge( @@ -6835,7 +6780,7 @@ public function find(string $collection, array $queries = [], string $forPermiss $nestedSelections = $this->processRelationshipQueries($relationships, $queries); // Convert relationship filter queries to SQL-level subqueries - $queriesOrNull = $this->convertRelationshipQueries($relationships, $queries); + $queriesOrNull = $this->convertRelationshipFiltersToSubqueries($relationships, $queries); // If conversion returns null, it means no documents can match (relationship filter found no matches) if ($queriesOrNull === null) { @@ -6865,7 +6810,6 @@ public function find(string $collection, array $queries = [], string $forPermiss } foreach ($results as $index => $node) { - $node = $this->adapter->castingAfter($collection, $node); $node = $this->casting($collection, $node); $node = $this->decode($collection, $node, $selections); @@ -7011,7 +6955,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) $queries = Query::groupByType($queries)['filters']; $queries = $this->convertQueries($collection, $queries); - $queriesOrNull = $this->convertRelationshipQueries($relationships, $queries); + $queriesOrNull = $this->convertRelationshipFiltersToSubqueries($relationships, $queries); if ($queriesOrNull === null) { return 0; @@ -7062,18 +7006,14 @@ public function sum(string $collection, string $attribute, array $queries = [], } } - $authorization = new Authorization(self::PERMISSION_READ); - if ($authorization->isValid($collection->getRead())) { - $skipAuth = true; - } - $relationships = \array_filter( $collection->getAttribute('attributes', []), fn (Document $attribute) => $attribute->getAttribute('type') === self::VAR_RELATIONSHIP ); $queries = $this->convertQueries($collection, $queries); - $queriesOrNull = $this->convertRelationshipQueries($relationships, $queries); + + $queriesOrNull = $this->convertRelationshipFiltersToSubqueries($relationships, $queries); // If conversion returns null, it means no documents can match (relationship filter found no matches) if ($queriesOrNull === null) { @@ -7082,8 +7022,7 @@ public function sum(string $collection, string $attribute, array $queries = [], $queries = $queriesOrNull; - $getSum = fn () => $this->adapter->sum($collection, $attribute, $queries, $max); - $sum = $skipAuth ?? false ? Authorization::skip($getSum) : $getSum(); + $sum = $this->adapter->sum($collection, $attribute, $queries, $max); $this->trigger(self::EVENT_DOCUMENT_SUM, $sum); @@ -7510,15 +7449,15 @@ public function getLimitForIndexes(): int * @throws QueryException * @throws \Utopia\Database\Exception */ - public function convertQueries(Document $collection, array $queries): array + public static function convertQueries(Document $collection, array $queries): array { foreach ($queries as $index => $query) { if ($query->isNested()) { - $values = $this->convertQueries($collection, $query->getValues()); + $values = self::convertQueries($collection, $query->getValues()); $query->setValues($values); } - $query = $this->convertQuery($collection, $query); + $query = self::convertQuery($collection, $query); $queries[$index] = $query; } @@ -7533,7 +7472,7 @@ public function convertQueries(Document $collection, array $queries): array * @throws QueryException * @throws \Utopia\Database\Exception */ - public function convertQuery(Document $collection, Query $query): Query + public static function convertQuery(Document $collection, Query $query): Query { /** * @var array $attributes @@ -7560,9 +7499,7 @@ public function convertQuery(Document $collection, Query $query): Query $values = $query->getValues(); foreach ($values as $valueIndex => $value) { try { - $values[$valueIndex] = $this->adapter->getSupportForUTCCasting() - ? $this->adapter->setUTCDatetime($value) - : DateTime::setTimezone($value); + $values[$valueIndex] = DateTime::setTimezone($value); } catch (\Throwable $e) { throw new QueryException($e->getMessage(), $e->getCode(), $e); } @@ -7914,7 +7851,7 @@ private function processNestedRelationshipPath(string $startCollection, array $q } /** - * Convert relationship queries to SQL-safe subqueries recursively + * Convert relationship filter queries to SQL-safe subqueries recursively * * Queries like Query::equal('author.name', ['Alice']) are converted to * Query::equal('author', []) @@ -7935,7 +7872,7 @@ private function processNestedRelationshipPath(string $startCollection, array $q * @param array $queries * @return array|null Returns null if relationship filters cannot match any documents */ - private function convertRelationshipQueries( + private function convertRelationshipFiltersToSubqueries( array $relationships, array $queries, ): ?array { diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index b63f71ab0..bab80c173 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -30,60 +30,29 @@ class Index extends Validator protected bool $spatialIndexOrderSupport; - protected bool $supportForAttributes; - - protected bool $multipleFulltextIndexSupport; - - protected bool $identicalIndexSupport; - - /** - * @var array $indexes - */ - protected array $indexes; - /** * @param array $attributes - * @param array $indexes * @param int $maxLength * @param array $reservedKeys * @param bool $arrayIndexSupport * @param bool $spatialIndexSupport * @param bool $spatialIndexNullSupport * @param bool $spatialIndexOrderSupport - * @param bool $supportForAttributes - * @param bool $multipleFulltextIndexSupport - * @param bool $identicalIndexSupport * @throws DatabaseException */ - public function __construct( - array $attributes, - array $indexes, - int $maxLength, - array $reservedKeys = [], - bool $arrayIndexSupport = false, - bool $spatialIndexSupport = false, - bool $spatialIndexNullSupport = false, - bool $spatialIndexOrderSupport = false, - bool $supportForAttributes = true, - bool $multipleFulltextIndexSupport = true, - bool $identicalIndexSupport = true - ) { + public function __construct(array $attributes, int $maxLength, array $reservedKeys = [], bool $arrayIndexSupport = false, bool $spatialIndexSupport = false, bool $spatialIndexNullSupport = false, bool $spatialIndexOrderSupport = false) + { $this->maxLength = $maxLength; $this->reservedKeys = $reservedKeys; $this->arrayIndexSupport = $arrayIndexSupport; $this->spatialIndexSupport = $spatialIndexSupport; $this->spatialIndexNullSupport = $spatialIndexNullSupport; $this->spatialIndexOrderSupport = $spatialIndexOrderSupport; - $this->supportForAttributes = $supportForAttributes; - $this->multipleFulltextIndexSupport = $multipleFulltextIndexSupport; - $this->identicalIndexSupport = $identicalIndexSupport; - $this->indexes = $indexes; foreach ($attributes as $attribute) { $key = \strtolower($attribute->getAttribute('key', $attribute->getAttribute('$id'))); $this->attributes[$key] = $attribute; } - foreach (Database::INTERNAL_ATTRIBUTES as $attribute) { $key = \strtolower($attribute['$id']); $this->attributes[$key] = new Document($attribute); @@ -106,7 +75,7 @@ public function getDescription(): string public function checkAttributesNotFound(Document $index): bool { foreach ($index->getAttribute('attributes', []) as $attribute) { - if ($this->supportForAttributes && !isset($this->attributes[\strtolower($attribute)])) { + if (!isset($this->attributes[\strtolower($attribute)])) { $this->message = 'Invalid index attribute "' . $attribute . '" not found'; return false; } @@ -154,14 +123,11 @@ public function checkDuplicatedAttributes(Document $index): bool */ public function checkFulltextIndexNonString(Document $index): bool { - if (!$this->supportForAttributes) { - return true; - } if ($index->getAttribute('type') === Database::INDEX_FULLTEXT) { foreach ($index->getAttribute('attributes', []) as $attribute) { $attribute = $this->attributes[\strtolower($attribute)] ?? new Document(); if ($attribute->getAttribute('type', '') !== Database::VAR_STRING) { - $this->message = 'Attribute "' . $attribute->getAttribute('key', $attribute->getAttribute('$id')) . '" cannot be part of a fulltext index, must be of type string'; + $this->message = 'Attribute "' . $attribute->getAttribute('key', $attribute->getAttribute('$id')) . '" cannot be part of a FULLTEXT index, must be of type string'; return false; } } @@ -175,9 +141,6 @@ public function checkFulltextIndexNonString(Document $index): bool */ public function checkArrayIndex(Document $index): bool { - if (!$this->supportForAttributes) { - return true; - } $attributes = $index->getAttribute('attributes', []); $orders = $index->getAttribute('orders', []); $lengths = $index->getAttribute('lengths', []); @@ -342,14 +305,6 @@ public function isValid($value): bool return false; } - if (!$this->checkMultipleFulltextIndex($value)) { - return false; - } - - if (!$this->checkIdenticalIndex($value)) { - return false; - } - return true; } @@ -380,83 +335,7 @@ public function getType(): string /** * @param Document $index * @return bool - */ - public function checkMultipleFulltextIndex(Document $index): bool - { - if ($this->multipleFulltextIndexSupport) { - return true; - } - - if ($index->getAttribute('type') === Database::INDEX_FULLTEXT) { - foreach ($this->indexes as $existingIndex) { - if ($existingIndex->getId() === $index->getId()) { - continue; - } - if ($existingIndex->getAttribute('type') === Database::INDEX_FULLTEXT) { - $this->message = 'There is already a fulltext index in the collection'; - return false; - } - } - } - - return true; - } - - /** - * @param Document $index - * @return bool - */ - public function checkIdenticalIndex(Document $index): bool - { - if ($this->identicalIndexSupport) { - return true; - } - - $indexAttributes = $index->getAttribute('attributes', []); - $indexOrders = $index->getAttribute('orders', []); - $indexType = $index->getAttribute('type', ''); - - foreach ($this->indexes as $existingIndex) { - $existingAttributes = $existingIndex->getAttribute('attributes', []); - $existingOrders = $existingIndex->getAttribute('orders', []); - $existingType = $existingIndex->getAttribute('type', ''); - - $attributesMatch = false; - if (empty(array_diff($existingAttributes, $indexAttributes)) && - empty(array_diff($indexAttributes, $existingAttributes))) { - $attributesMatch = true; - } - - $ordersMatch = false; - if (empty(array_diff($existingOrders, $indexOrders)) && - empty(array_diff($indexOrders, $existingOrders))) { - $ordersMatch = true; - } - - if ($attributesMatch && $ordersMatch) { - // Allow fulltext + key/unique combinations (different purposes) - $regularTypes = [Database::INDEX_KEY, Database::INDEX_UNIQUE]; - $isRegularIndex = in_array($indexType, $regularTypes); - $isRegularExisting = in_array($existingType, $regularTypes); - - // Only reject if both are regular index types (key or unique) - if ($isRegularIndex && $isRegularExisting) { - $this->message = 'There is already an index with the same attributes and orders'; - return false; - } - - // Allow if one is fulltext/spatial and other is key/unique - } - } - - return true; - } - - - /** - * @param Document $index - * @return bool - */ + */ public function checkSpatialIndex(Document $index): bool { $type = $index->getAttribute('type'); @@ -498,6 +377,7 @@ public function checkSpatialIndex(Document $index): bool } } + return true; } } diff --git a/src/Database/Validator/Key.php b/src/Database/Validator/Key.php index 1d180c38c..ad1c5df4e 100644 --- a/src/Database/Validator/Key.php +++ b/src/Database/Validator/Key.php @@ -8,15 +8,10 @@ class Key extends Validator { protected bool $allowInternal = false; // If true, you keys starting with $ are allowed - /** - * Maximum length for Key validation - */ - protected const KEY_MAX_LENGTH = 255; - /** * @var string */ - protected string $message = 'Parameter must contain at most ' . self::KEY_MAX_LENGTH . ' chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char'; + protected string $message = 'Parameter must contain at most 36 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char'; /** * Get Description. @@ -81,8 +76,8 @@ public function isValid($value): bool if (\preg_match('/[^A-Za-z0-9\_\-\.]/', $value)) { return false; } - // At most KEY_MAX_LENGTH chars - if (\mb_strlen($value) > self::KEY_MAX_LENGTH) { + + if (\mb_strlen($value) > 36) { return false; } diff --git a/src/Database/Validator/Label.php b/src/Database/Validator/Label.php index 40e576a88..6cc4f031f 100644 --- a/src/Database/Validator/Label.php +++ b/src/Database/Validator/Label.php @@ -4,7 +4,7 @@ class Label extends Key { - protected string $message = 'Value must be a valid string between 1 and ' . self::KEY_MAX_LENGTH . ' chars containing only alphanumeric chars'; + protected string $message = 'Value must be a valid string between 1 and 36 chars containing only alphanumeric chars'; /** * Is valid. diff --git a/src/Database/Validator/Queries/Documents.php b/src/Database/Validator/Queries/Documents.php index ebdfdf05a..92d4e4e69 100644 --- a/src/Database/Validator/Queries/Documents.php +++ b/src/Database/Validator/Queries/Documents.php @@ -30,7 +30,6 @@ public function __construct( int $maxValuesCount = 5000, \DateTime $minAllowedDate = new \DateTime('0000-01-01'), \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), - bool $supportForAttributes = true ) { $attributes[] = new Document([ '$id' => '$id', @@ -67,10 +66,9 @@ public function __construct( $maxValuesCount, $minAllowedDate, $maxAllowedDate, - $supportForAttributes ), - new Order($attributes, $supportForAttributes), - new Select($attributes, $supportForAttributes), + new Order($attributes), + new Select($attributes), ]; parent::__construct($attributes, $indexes, $validators); diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index 350b4877c..5bc973f22 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -31,7 +31,6 @@ public function __construct( private readonly int $maxValuesCount = 5000, private readonly \DateTime $minAllowedDate = new \DateTime('0000-01-01'), private readonly \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), - private bool $supportForAttributes = true ) { foreach ($attributes as $attribute) { $this->schema[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); @@ -63,7 +62,7 @@ protected function isValidAttribute(string $attribute): bool } // Search for attribute in schema - if ($this->supportForAttributes && !isset($this->schema[$attribute])) { + if (!isset($this->schema[$attribute])) { $this->message = 'Attribute not found in schema: ' . $attribute; return false; } @@ -91,15 +90,6 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s $attribute = \explode('.', $attribute)[0]; } - if (!$this->supportForAttributes && !isset($this->schema[$attribute])) { - // First check maxValuesCount guard for any IN-style value arrays - if (count($values) > $this->maxValuesCount) { - $this->message = 'Query on attribute has greater than ' . $this->maxValuesCount . ' values: ' . $attribute; - return false; - } - - return true; - } $attributeSchema = $this->schema[$attribute]; // Skip value validation for nested relationship queries (e.g., author.age) diff --git a/src/Database/Validator/Query/Order.php b/src/Database/Validator/Query/Order.php index 5d9970a01..90f7746b2 100644 --- a/src/Database/Validator/Query/Order.php +++ b/src/Database/Validator/Query/Order.php @@ -14,9 +14,8 @@ class Order extends Base /** * @param array $attributes - * @param bool $supportForAttributes */ - public function __construct(array $attributes = [], protected bool $supportForAttributes = true) + public function __construct(array $attributes = []) { foreach ($attributes as $attribute) { $this->schema[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); @@ -46,7 +45,7 @@ protected function isValidAttribute(string $attribute): bool } // Search for attribute in schema - if ($this->supportForAttributes && !isset($this->schema[$attribute])) { + if (!isset($this->schema[$attribute])) { $this->message = 'Attribute not found in schema: ' . $attribute; return false; } diff --git a/src/Database/Validator/Query/Select.php b/src/Database/Validator/Query/Select.php index b0ed9e564..40572b828 100644 --- a/src/Database/Validator/Query/Select.php +++ b/src/Database/Validator/Query/Select.php @@ -29,9 +29,8 @@ class Select extends Base /** * @param array $attributes - * @param bool $supportForAttributes */ - public function __construct(array $attributes = [], protected bool $supportForAttributes = true) + public function __construct(array $attributes = []) { foreach ($attributes as $attribute) { $this->schema[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); @@ -90,7 +89,7 @@ public function isValid($value): bool continue; } - if ($this->supportForAttributes && !isset($this->schema[$attribute]) && $attribute !== '*') { + if (!isset($this->schema[$attribute]) && $attribute !== '*') { $this->message = 'Attribute not found in schema: ' . $attribute; return false; } diff --git a/src/Database/Validator/Sequence.php b/src/Database/Validator/Sequence.php index 21c77d4e0..305632727 100644 --- a/src/Database/Validator/Sequence.php +++ b/src/Database/Validator/Sequence.php @@ -46,8 +46,9 @@ public function isValid($value): bool } switch ($this->idAttributeType) { - case Database::VAR_UUID7: //UUID7 - return preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-7[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i', $value) === 1; + case Database::VAR_OBJECT_ID: + return preg_match('/^[a-f0-9]{24}$/i', $value) === 1; + case Database::VAR_INTEGER: $start = ($this->primary) ? 1 : 0; $validator = new Range($start, Database::BIG_INT_MAX, Database::VAR_INTEGER); diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index 421eafd83..cfb12fa3a 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -106,7 +106,6 @@ public function __construct( private readonly string $idAttributeType, private readonly \DateTime $minAllowedDate = new \DateTime('0000-01-01'), private readonly \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), - private bool $supportForAttributes = true ) { } @@ -252,11 +251,7 @@ public function isValid($document): bool */ protected function checkForAllRequiredValues(array $structure, array $attributes, array &$keys): bool { - if (!$this->supportForAttributes) { - return true; - } - - foreach ($attributes as $attribute) { // Check all required attributes are set + foreach ($attributes as $key => $attribute) { // Check all required attributes are set $name = $attribute['$id'] ?? ''; $required = $attribute['required'] ?? false; @@ -281,9 +276,6 @@ protected function checkForAllRequiredValues(array $structure, array $attributes */ protected function checkForUnknownAttributes(array $structure, array $keys): bool { - if (!$this->supportForAttributes) { - return true; - } foreach ($structure as $key => $value) { if (!array_key_exists($key, $keys)) { // Check no unknown attributes are set $this->message = 'Unknown attribute: "'.$key.'"'; @@ -365,10 +357,8 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys) break; default: - if ($this->supportForAttributes) { - $this->message = 'Unknown attribute type "'.$type.'"'; - return false; - } + $this->message = 'Unknown attribute type "'.$type.'"'; + return false; } /** Error message label, either 'format' or 'type' */ diff --git a/src/Database/Validator/UID.php b/src/Database/Validator/UID.php index 45971da66..34d466e34 100644 --- a/src/Database/Validator/UID.php +++ b/src/Database/Validator/UID.php @@ -13,6 +13,6 @@ class UID extends Key */ public function getDescription(): string { - return 'UID must contain at most ' . self::KEY_MAX_LENGTH . ' chars. Valid chars are a-z, A-Z, 0-9, and underscore. Can\'t start with a leading underscore'; + return 'UID must contain at most 36 chars. Valid chars are a-z, A-Z, 0-9, and underscore. Can\'t start with a leading underscore'; } } diff --git a/tests/e2e/Adapter/MongoDBTest.php b/tests/e2e/Adapter/MongoDBTest.php deleted file mode 100644 index 55b21f8e4..000000000 --- a/tests/e2e/Adapter/MongoDBTest.php +++ /dev/null @@ -1,108 +0,0 @@ -connect('redis', 6379); - $redis->flushAll(); - $cache = new Cache(new RedisAdapter($redis)); - - $schema = 'utopiaTests'; // same as $this->testDatabase - $client = new Client( - $schema, - 'mongo', - 27017, - 'root', - 'password', - false - ); - - $database = new Database(new Mongo($client), $cache); - $database - ->setDatabase($schema) - ->setNamespace(static::$namespace = 'myapp_' . uniqid()); - - if ($database->exists()) { - $database->delete(); - } - - $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); - } - - protected static function deleteColumn(string $collection, string $column): bool - { - return true; - } - - protected static function deleteIndex(string $collection, string $index): bool - { - return true; - } -} diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index 53bdd54e7..89ab81a50 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -392,11 +392,6 @@ public function testUpdateAttributeRequired(): void /** @var Database $database */ $database = static::getDatabase(); - if (!$database->getAdapter()->getSupportForAttributes()) { - $this->expectNotToPerformAssertions(); - return; - } - $database->updateAttributeRequired('flowers', 'inStock', true); $this->expectExceptionMessage('Invalid document structure: Missing required attribute "inStock"'); @@ -453,10 +448,7 @@ public function testUpdateAttributeFormat(): void { /** @var Database $database */ $database = static::getDatabase(); - if (!$database->getAdapter()->getSupportForAttributes()) { - $this->expectNotToPerformAssertions(); - return; - } + $database->createAttribute('flowers', 'price', Database::VAR_INTEGER, 0, false); $doc = $database->createDocument('flowers', new Document([ @@ -656,10 +648,6 @@ public function testUpdateAttributeRename(): void { /** @var Database $database */ $database = static::getDatabase(); - if (!$database->getAdapter()->getSupportForAttributes()) { - $this->expectNotToPerformAssertions(); - return; - } $database->createCollection('rename_test'); @@ -705,27 +693,12 @@ public function testUpdateAttributeRename(): void $this->assertEquals('renamed', $collection->getAttribute('attributes')[0]['$id']); $this->assertEquals('renamed', $collection->getAttribute('indexes')[0]['attributes'][0]); - $supportsIdenticalIndexes = $database->getAdapter()->getSupportForIdenticalIndexes(); - - try { - // Check empty newKey doesn't cause issues - $database->updateAttribute( - collection: 'rename_test', - id: 'renamed', - type: Database::VAR_STRING, - ); - - if (!$supportsIdenticalIndexes) { - $this->fail('Expected exception when getSupportForIdenticalIndexes=false but none was thrown'); - } - } catch (Throwable $e) { - if (!$supportsIdenticalIndexes) { - $this->assertTrue(true, 'Exception thrown as expected when getSupportForIdenticalIndexes=false'); - return; // Exit early if exception was expected - } else { - $this->fail('Unexpected exception when getSupportForIdenticalIndexes=true: ' . $e->getMessage()); - } - } + // Check empty newKey doesn't cause issues + $database->updateAttribute( + collection: 'rename_test', + id: 'renamed', + type: Database::VAR_STRING, + ); $collection = $database->getCollection('rename_test'); @@ -1328,7 +1301,7 @@ public function testArrayAttribute(): void $collection, 'tv_show', Database::VAR_STRING, - size: $database->getAdapter()->getMaxIndexLength() - 68, + size: 700, required: false, signed: false, )); @@ -1357,9 +1330,7 @@ public function testArrayAttribute(): void $database->createDocument($collection, new Document([])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - if ($database->getAdapter()->getSupportForAttributes()) { - $this->assertEquals('Invalid document structure: Missing required attribute "booleans"', $e->getMessage()); - } + $this->assertEquals('Invalid document structure: Missing required attribute "booleans"', $e->getMessage()); } $database->updateAttribute($collection, 'booleans', required: false); @@ -1379,9 +1350,7 @@ public function testArrayAttribute(): void ])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - if ($database->getAdapter()->getSupportForAttributes()) { - $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()); - } + $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 { @@ -1390,9 +1359,7 @@ public function testArrayAttribute(): void ])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - if ($database->getAdapter()->getSupportForAttributes()) { - $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()); - } + $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 { @@ -1401,9 +1368,7 @@ public function testArrayAttribute(): void ])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - if ($database->getAdapter()->getSupportForAttributes()) { - $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid integer', $e->getMessage()); - } + $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid integer', $e->getMessage()); } try { @@ -1412,9 +1377,7 @@ public function testArrayAttribute(): void ])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - if ($database->getAdapter()->getSupportForAttributes()) { - $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid range between 0 and 2,147,483,647', $e->getMessage()); - } + $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid range between 0 and 2,147,483,647', $e->getMessage()); } $database->createDocument($collection, new Document([ @@ -1441,7 +1404,7 @@ public function testArrayAttribute(): void if ($database->getAdapter()->getSupportForIndexArray()) { /** - * Functional index dependency cannot be dropped or rename + * functional index dependency cannot be dropped or rename */ $database->createIndex($collection, 'idx_cards', Database::INDEX_KEY, ['cards'], [100]); } @@ -1488,9 +1451,7 @@ public function testArrayAttribute(): void if ($database->getAdapter()->getSupportForIndexArray()) { try { $database->createIndex($collection, 'indx', Database::INDEX_FULLTEXT, ['names']); - if ($database->getAdapter()->getSupportForAttributes()) { - $this->fail('Failed to throw exception'); - } + $this->fail('Failed to throw exception'); } catch (Throwable $e) { if ($database->getAdapter()->getSupportForFulltextIndex()) { $this->assertEquals('"Fulltext" index is forbidden on array attributes', $e->getMessage()); @@ -1501,15 +1462,9 @@ public function testArrayAttribute(): void try { $database->createIndex($collection, 'indx', Database::INDEX_KEY, ['numbers', 'names'], [100,100]); - if ($database->getAdapter()->getSupportForAttributes()) { - $this->fail('Failed to throw exception'); - } + $this->fail('Failed to throw exception'); } catch (Throwable $e) { - if ($database->getAdapter()->getSupportForAttributes()) { - $this->assertEquals('An index may only contain one array attribute', $e->getMessage()); - } else { - $this->assertEquals('Index already exists', $e->getMessage()); - } + $this->assertEquals('An index may only contain one array attribute', $e->getMessage()); } } @@ -1523,10 +1478,11 @@ public function testArrayAttribute(): void )); if ($database->getAdapter()->getSupportForIndexArray()) { + + if ($database->getAdapter()->getMaxIndexLength() > 0) { // If getMaxIndexLength() > 0 We clear length for array attributes $database->createIndex($collection, 'indx1', Database::INDEX_KEY, ['long_size'], [], []); - $database->deleteIndex($collection, 'indx1'); $database->createIndex($collection, 'indx2', Database::INDEX_KEY, ['long_size'], [1000], []); try { @@ -1537,11 +1493,12 @@ public function testArrayAttribute(): void } } + // We clear orders for array attributes + $database->createIndex($collection, 'indx3', Database::INDEX_KEY, ['names'], [255], ['desc']); + try { - if ($database->getAdapter()->getSupportForAttributes()) { - $database->createIndex($collection, 'indx4', Database::INDEX_KEY, ['age', 'names'], [10, 255], []); - $this->fail('Failed to throw exception'); - } + $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()); } @@ -1608,22 +1565,17 @@ public function testCreateDatetime(): void $database = static::getDatabase(); $database->createCollection('datetime'); - if ($database->getAdapter()->getSupportForAttributes()) { - $this->assertEquals(true, $database->createAttribute('datetime', 'date', Database::VAR_DATETIME, 0, true, null, true, false, null, [], ['datetime'])); - $this->assertEquals(true, $database->createAttribute('datetime', 'date2', Database::VAR_DATETIME, 0, false, null, true, false, null, [], ['datetime'])); - } + + $this->assertEquals(true, $database->createAttribute('datetime', 'date', Database::VAR_DATETIME, 0, true, null, true, false, null, [], ['datetime'])); + $this->assertEquals(true, $database->createAttribute('datetime', 'date2', Database::VAR_DATETIME, 0, false, null, true, false, null, [], ['datetime'])); try { $database->createDocument('datetime', new Document([ 'date' => ['2020-01-01'], // array ])); - if ($database->getAdapter()->getSupportForAttributes()) { - $this->fail('Failed to throw exception'); - } + $this->fail('Failed to throw exception'); } catch (Exception $e) { - if ($database->getAdapter()->getSupportForAttributes()) { - $this->assertInstanceOf(StructureException::class, $e); - } + $this->assertInstanceOf(StructureException::class, $e); } $doc = $database->createDocument('datetime', new Document([ @@ -1666,29 +1618,20 @@ public function testCreateDatetime(): void try { $database->createDocument('datetime', new Document([ - '$id' => 'datenew1', - 'date' => "1975-12-06 00:00:61", // 61 seconds is invalid, + 'date' => "1975-12-06 00:00:61" // 61 seconds is invalid ])); - if ($database->getAdapter()->getSupportForAttributes()) { - $this->fail('Failed to throw exception'); - } + $this->fail('Failed to throw exception'); } catch (Exception $e) { - if ($database->getAdapter()->getSupportForAttributes()) { - $this->assertInstanceOf(StructureException::class, $e); - } + $this->assertInstanceOf(StructureException::class, $e); } try { $database->createDocument('datetime', new Document([ 'date' => '+055769-02-14T17:56:18.000Z' ])); - if ($database->getAdapter()->getSupportForAttributes()) { - $this->fail('Failed to throw exception'); - } + $this->fail('Failed to throw exception'); } catch (Exception $e) { - if ($database->getAdapter()->getSupportForAttributes()) { - $this->assertInstanceOf(StructureException::class, $e); - } + $this->assertInstanceOf(StructureException::class, $e); } $invalidDates = [ @@ -1712,9 +1655,7 @@ public function testCreateDatetime(): void $database->find('datetime', [ Query::equal('date', [$date]) ]); - if ($database->getAdapter()->getSupportForAttributes()) { - $this->fail('Failed to throw exception'); - } + $this->fail('Failed to throw exception'); } catch (Throwable $e) { $this->assertTrue($e instanceof QueryException); $this->assertEquals('Invalid query: Query value is invalid for attribute "date"', $e->getMessage()); diff --git a/tests/e2e/Adapter/Scopes/CollectionTests.php b/tests/e2e/Adapter/Scopes/CollectionTests.php index c8588b836..5178a414d 100644 --- a/tests/e2e/Adapter/Scopes/CollectionTests.php +++ b/tests/e2e/Adapter/Scopes/CollectionTests.php @@ -680,10 +680,10 @@ public function testCreateCollectionWithSchemaIndexes(): void 'orders' => [], ]), new Document([ - '$id' => ID::custom('idx_username_uid'), + '$id' => ID::custom('idx_username_created_at'), 'type' => Database::INDEX_KEY, - 'attributes' => ['username', '$id'], // to solve the same attribute mongo issue - 'lengths' => [99, 200], // Length not equal to attributes length + 'attributes' => ['username'], + 'lengths' => [99], // Length not equal to attributes length 'orders' => [Database::ORDER_DESC], ]), ]; diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 6be6f0f77..8f53a9c23 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -30,9 +30,6 @@ public function testBigintSequence(): void $database->createCollection(__FUNCTION__); $sequence = 5_000_000_000_000_000; - if ($database->getAdapter()->getIdAttributeType() == Database::VAR_UUID7) { - $sequence = '01995753-881b-78cf-9506-2cffecf8f227'; - } $document = $database->createDocument(__FUNCTION__, new Document([ '$sequence' => (string)$sequence, @@ -70,11 +67,6 @@ public function testCreateDocument(): Document $this->assertEquals(true, $database->createAttribute('documents', 'with-dash', Database::VAR_STRING, 128, false, null)); $this->assertEquals(true, $database->createAttribute('documents', 'id', Database::VAR_ID, 0, false, null)); - $sequence = '1000000'; - if ($database->getAdapter()->getIdAttributeType() == Database::VAR_UUID7) { - $sequence = '01890dd5-7331-7f3a-9c1b-123456789abc' ; - } - $document = $database->createDocument('documents', new Document([ '$permissions' => [ Permission::read(Role::any()), @@ -101,10 +93,10 @@ public function testCreateDocument(): Document 'colors' => ['pink', 'green', 'blue'], 'empty' => [], 'with-dash' => 'Works', - 'id' => $sequence, + 'id' => '1000000', ])); - $this->assertNotEmpty($document->getId()); + $this->assertNotEmpty(true, $document->getId()); $this->assertIsString($document->getAttribute('string')); $this->assertEquals('text📝', $document->getAttribute('string')); // Also makes sure an emoji is working $this->assertIsInt($document->getAttribute('integer_signed')); @@ -126,18 +118,12 @@ public function testCreateDocument(): Document $this->assertEquals([], $document->getAttribute('empty')); $this->assertEquals('Works', $document->getAttribute('with-dash')); $this->assertIsString($document->getAttribute('id')); - $this->assertEquals($sequence, $document->getAttribute('id')); - - - $sequence = '56000'; - if ($database->getAdapter()->getIdAttributeType() == Database::VAR_UUID7) { - $sequence = '01890dd5-7331-7f3a-9c1b-123456789def' ; - } + $this->assertEquals('1000000', $document->getAttribute('id')); // Test create document with manual internal id $manualIdDocument = $database->createDocument('documents', new Document([ '$id' => '56000', - '$sequence' => $sequence, + '$sequence' => '56000', '$permissions' => [ Permission::read(Role::any()), Permission::read(Role::user(ID::custom('1'))), @@ -165,8 +151,8 @@ public function testCreateDocument(): Document 'with-dash' => 'Works', ])); - $this->assertEquals($sequence, $manualIdDocument->getSequence()); - $this->assertNotEmpty($manualIdDocument->getId()); + $this->assertEquals('56000', $manualIdDocument->getSequence()); + $this->assertNotEmpty(true, $manualIdDocument->getId()); $this->assertIsString($manualIdDocument->getAttribute('string')); $this->assertEquals('text📝', $manualIdDocument->getAttribute('string')); // Also makes sure an emoji is working $this->assertIsInt($manualIdDocument->getAttribute('integer_signed')); @@ -191,8 +177,8 @@ public function testCreateDocument(): Document $manualIdDocument = $database->getDocument('documents', '56000'); - $this->assertEquals($sequence, $manualIdDocument->getSequence()); - $this->assertNotEmpty($manualIdDocument->getId()); + $this->assertEquals('56000', $manualIdDocument->getSequence()); + $this->assertNotEmpty(true, $manualIdDocument->getId()); $this->assertIsString($manualIdDocument->getAttribute('string')); $this->assertEquals('text📝', $manualIdDocument->getAttribute('string')); // Also makes sure an emoji is working $this->assertIsInt($manualIdDocument->getAttribute('integer_signed')); @@ -229,10 +215,8 @@ public function testCreateDocument(): Document ])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - if ($database->getAdapter()->getSupportForAttributes()) { - $this->assertTrue($e instanceof StructureException); - $this->assertStringContainsString('Invalid document structure: Attribute "float_unsigned" has invalid type. Value must be a valid range between 0 and', $e->getMessage()); - } + $this->assertTrue($e instanceof StructureException); + $this->assertStringContainsString('Invalid document structure: Attribute "float_unsigned" has invalid type. Value must be a valid range between 0 and', $e->getMessage()); } try { @@ -250,10 +234,8 @@ public function testCreateDocument(): Document ])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - if ($database->getAdapter()->getSupportForAttributes()) { - $this->assertTrue($e instanceof StructureException); - $this->assertEquals('Invalid document structure: Attribute "bigint_unsigned" has invalid type. Value must be a valid range between 0 and 9,223,372,036,854,775,807', $e->getMessage()); - } + $this->assertTrue($e instanceof StructureException); + $this->assertEquals('Invalid document structure: Attribute "bigint_unsigned" has invalid type. Value must be a valid range between 0 and 9,223,372,036,854,775,807', $e->getMessage()); } try { @@ -274,10 +256,8 @@ public function testCreateDocument(): Document ])); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - if ($database->getAdapter()->getSupportForAttributes()) { - $this->assertTrue($e instanceof StructureException); - $this->assertEquals('Invalid document structure: Attribute "$sequence" has invalid type. Invalid sequence value', $e->getMessage()); - } + $this->assertTrue($e instanceof StructureException); + $this->assertEquals('Invalid document structure: Attribute "$sequence" has invalid type. Invalid sequence value', $e->getMessage()); } /** @@ -299,29 +279,24 @@ public function testCreateDocument(): Document 'empty' => [], 'with-dash' => '', ])); - $this->assertNotEmpty($documentIdNull->getSequence()); + $this->assertNotEmpty(true, $documentIdNull->getSequence()); $this->assertNull($documentIdNull->getAttribute('id')); $documentIdNull = $database->getDocument('documents', $documentIdNull->getId()); - $this->assertNotEmpty($documentIdNull->getId()); + $this->assertNotEmpty(true, $documentIdNull->getId()); $this->assertNull($documentIdNull->getAttribute('id')); $documentIdNull = $database->findOne('documents', [ query::isNull('id') ]); - $this->assertNotEmpty($documentIdNull->getId()); + $this->assertNotEmpty(true, $documentIdNull->getId()); $this->assertNull($documentIdNull->getAttribute('id')); - $sequence = '0'; - if ($database->getAdapter()->getIdAttributeType() == Database::VAR_UUID7) { - $sequence = '01890dd5-7331-7f3a-9c1b-123456789abc'; - } - /** * Insert ID attribute with '0' */ $documentId0 = $database->createDocument('documents', new Document([ - 'id' => $sequence, + 'id' => '0', '$permissions' => [Permission::read(Role::any())], 'string' => '', 'integer_signed' => 1, @@ -335,22 +310,21 @@ public function testCreateDocument(): Document 'empty' => [], 'with-dash' => '', ])); - $this->assertNotEmpty($documentId0->getSequence()); - + $this->assertNotEmpty(true, $documentId0->getSequence()); $this->assertIsString($documentId0->getAttribute('id')); - $this->assertEquals($sequence, $documentId0->getAttribute('id')); + $this->assertEquals('0', $documentId0->getAttribute('id')); $documentId0 = $database->getDocument('documents', $documentId0->getId()); - $this->assertNotEmpty($documentId0->getSequence()); + $this->assertNotEmpty(true, $documentId0->getSequence()); $this->assertIsString($documentId0->getAttribute('id')); - $this->assertEquals($sequence, $documentId0->getAttribute('id')); + $this->assertEquals('0', $documentId0->getAttribute('id')); $documentId0 = $database->findOne('documents', [ - query::equal('id', [$sequence]) + query::equal('id', ['0']) ]); - $this->assertNotEmpty($documentId0->getSequence()); + $this->assertNotEmpty(true, $documentId0->getSequence()); $this->assertIsString($documentId0->getAttribute('id')); - $this->assertEquals($sequence, $documentId0->getAttribute('id')); + $this->assertEquals('0', $documentId0->getAttribute('id')); return $document; @@ -425,7 +399,7 @@ public function testCreateDocuments(): void $this->assertEquals($count, \count($results)); foreach ($results as $document) { - $this->assertNotEmpty($document->getId()); + $this->assertNotEmpty(true, $document->getId()); $this->assertIsString($document->getAttribute('string')); $this->assertEquals('text📝', $document->getAttribute('string')); // Also makes sure an emoji is working $this->assertIsInt($document->getAttribute('integer')); @@ -441,7 +415,7 @@ public function testCreateDocuments(): void $this->assertEquals($count, \count($documents)); foreach ($documents as $document) { - $this->assertNotEmpty($document->getId()); + $this->assertNotEmpty(true, $document->getId()); $this->assertIsString($document->getAttribute('string')); $this->assertEquals('text📝', $document->getAttribute('string')); // Also makes sure an emoji is working $this->assertIsInt($document->getAttribute('integer')); @@ -462,19 +436,12 @@ public function testCreateDocumentsWithAutoIncrement(): void /** @var array $documents */ $documents = []; - $offset = 1000000; - for ($i = $offset; $i <= ($offset + 10); $i++) { - $sequence = (string)$i; - if ($database->getAdapter()->getIdAttributeType() == Database::VAR_UUID7) { - // Replace last 6 digits with $i to make it unique - $suffix = str_pad(substr((string)$i, -6), 6, '0', STR_PAD_LEFT); - $sequence = '01890dd5-7331-7f3a-9c1b-123456' . $suffix; - } - - $hash[$i] = $sequence; + $count = 10; + $sequence = 1_000_000; + for ($i = $sequence; $i <= ($sequence + $count); $i++) { $documents[] = new Document([ - '$sequence' => $sequence, + '$sequence' => (string)$i, '$permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), @@ -491,10 +458,9 @@ public function testCreateDocumentsWithAutoIncrement(): void $documents = $database->find(__FUNCTION__, [ Query::orderAsc() ]); - foreach ($documents as $index => $document) { - $this->assertEquals($hash[$index + $offset], $document->getSequence()); - $this->assertNotEmpty($document->getId()); + $this->assertEquals($sequence + $index, $document->getSequence()); + $this->assertNotEmpty(true, $document->getId()); $this->assertEquals('text', $document->getAttribute('string')); } } @@ -695,8 +661,8 @@ public function testUpsertDocuments(): void $createdAt = []; foreach ($results as $index => $document) { $createdAt[$index] = $document->getCreatedAt(); - $this->assertNotEmpty($document->getId()); - $this->assertNotEmpty($document->getSequence()); + $this->assertNotEmpty(true, $document->getId()); + $this->assertNotEmpty(true, $document->getSequence()); $this->assertIsString($document->getAttribute('string')); $this->assertEquals('text📝', $document->getAttribute('string')); // Also makes sure an emoji is working $this->assertIsInt($document->getAttribute('integer')); @@ -710,7 +676,7 @@ public function testUpsertDocuments(): void $this->assertEquals(2, count($documents)); foreach ($documents as $document) { - $this->assertNotEmpty($document->getId()); + $this->assertNotEmpty(true, $document->getId()); $this->assertIsString($document->getAttribute('string')); $this->assertEquals('text📝', $document->getAttribute('string')); // Also makes sure an emoji is working $this->assertIsInt($document->getAttribute('integer')); @@ -732,8 +698,8 @@ public function testUpsertDocuments(): void $this->assertEquals(2, $count); foreach ($results as $document) { - $this->assertNotEmpty($document->getId()); - $this->assertNotEmpty($document->getSequence()); + $this->assertNotEmpty(true, $document->getId()); + $this->assertNotEmpty(true, $document->getSequence()); $this->assertIsString($document->getAttribute('string')); $this->assertEquals('new text📝', $document->getAttribute('string')); // Also makes sure an emoji is working $this->assertIsInt($document->getAttribute('integer')); @@ -748,7 +714,7 @@ public function testUpsertDocuments(): void foreach ($documents as $index => $document) { $this->assertEquals($createdAt[$index], $document->getCreatedAt()); - $this->assertNotEmpty($document->getId()); + $this->assertNotEmpty(true, $document->getId()); $this->assertIsString($document->getAttribute('string')); $this->assertEquals('new text📝', $document->getAttribute('string')); // Also makes sure an emoji is working $this->assertIsInt($document->getAttribute('integer')); @@ -968,9 +934,7 @@ public function testUpsertDocumentsAttributeMismatch(): void ]); $this->fail('Failed to throw exception'); } catch (Throwable $e) { - if ($database->getAdapter()->getSupportForAttributes()) { - $this->assertTrue($e instanceof StructureException, $e->getMessage()); - } + $this->assertTrue($e instanceof StructureException, $e->getMessage()); } // Ensure missing optionals on existing document is allowed @@ -1164,7 +1128,7 @@ public function testRespectNulls(): Document ], ])); - $this->assertNotEmpty($document->getId()); + $this->assertNotEmpty(true, $document->getId()); $this->assertNull($document->getAttribute('string')); $this->assertNull($document->getAttribute('integer')); $this->assertNull($document->getAttribute('bigint')); @@ -1203,7 +1167,7 @@ public function testCreateDocumentDefaults(): void $this->assertEquals('update("any")', $document2->getPermissions()[2]); $this->assertEquals('delete("any")', $document2->getPermissions()[3]); - $this->assertNotEmpty($document->getId()); + $this->assertNotEmpty(true, $document->getId()); $this->assertIsString($document->getAttribute('string')); $this->assertEquals('default', $document->getAttribute('string')); $this->assertIsInt($document->getAttribute('integer')); @@ -1365,7 +1329,7 @@ public function testGetDocument(Document $document): Document $document = $database->getDocument('documents', $document->getId()); - $this->assertNotEmpty($document->getId()); + $this->assertNotEmpty(true, $document->getId()); $this->assertIsString($document->getAttribute('string')); $this->assertEquals('text📝', $document->getAttribute('string')); $this->assertIsInt($document->getAttribute('integer_signed')); @@ -3554,11 +3518,6 @@ public function testFindOrderRandom(): void /** @var Database $database */ $database = static::getDatabase(); - if (!$database->getAdapter()->getSupportForOrderRandom()) { - $this->expectNotToPerformAssertions(); - return; - } - // Test orderRandom with default limit $documents = $database->find('movies', [ Query::orderRandom(), @@ -4218,7 +4177,7 @@ public function testUpdateDocument(Document $document): Document $new = $this->getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document); - $this->assertNotEmpty($new->getId()); + $this->assertNotEmpty(true, $new->getId()); $this->assertIsString($new->getAttribute('string')); $this->assertEquals('text📝 updated', $new->getAttribute('string')); $this->assertIsInt($new->getAttribute('integer_signed')); @@ -5259,19 +5218,16 @@ public function testFulltextIndexWithInteger(): void { /** @var Database $database */ $database = static::getDatabase(); - if ($database->getAdapter()->getSupportForAttributes()) { - $this->expectException(Exception::class); - if (!$this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { - $this->expectExceptionMessage('Fulltext index is not supported'); - } else { - $this->expectExceptionMessage('Attribute "integer_signed" cannot be part of a fulltext index, must be of type string'); - } - $database->createIndex('documents', 'fulltext_integer', Database::INDEX_FULLTEXT, ['string','integer_signed']); + $this->expectException(Exception::class); + + if (!$this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { + $this->expectExceptionMessage('Fulltext index is not supported'); } else { - $this->expectNotToPerformAssertions(); - return; + $this->expectExceptionMessage('Attribute "integer_signed" cannot be part of a FULLTEXT index, must be of type string'); } + + $database->createIndex('documents', 'fulltext_integer', Database::INDEX_FULLTEXT, ['string','integer_signed']); } public function testEnableDisableValidation(): void @@ -5355,13 +5311,8 @@ public function testExceptionCaseInsensitiveDuplicate(Document $document): Docum /** @var Database $database */ $database = static::getDatabase(); - $sequence = '200'; - if ($database->getAdapter()->getIdAttributeType() == Database::VAR_UUID7) { - $sequence = '01890dd5-7331-7f3a-9c1b-123456789abc' ; - } - $document->setAttribute('$id', 'caseSensitive'); - $document->setAttribute('$sequence', $sequence); + $document->setAttribute('$sequence', '200'); $database->createDocument($document->getCollection(), $document); $document->setAttribute('$id', 'CaseSensitive'); @@ -6164,106 +6115,6 @@ public function testCreateUpdateDocumentsMismatch(): void } $database->deleteCollection($colName); } - - public function testSchemalessDocumentOperation(): void - { - /** @var Database $database */ - $database = static::getDatabase(); - - if ($database->getAdapter()->getSupportForAttributes()) { - $this->expectNotToPerformAssertions(); - return; - } - - $colName = uniqid("schemaless"); - $database->createCollection($colName); - $database->createAttribute($colName, 'key', Database::VAR_STRING, 50, true); - $database->createAttribute($colName, 'value', Database::VAR_STRING, 50, false, 'value'); - - $permissions = [Permission::read(Role::any()), Permission::write(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())]; - - // Valid documents without any predefined attributes - $docs = [ - new Document(['$id' => 'doc1', '$permissions' => $permissions, 'freeA' => 'doc1']), - new Document(['$id' => 'doc2', '$permissions' => $permissions, 'freeB' => 'test']), - new Document(['$id' => 'doc3', '$permissions' => $permissions]), - ]; - $this->assertEquals(3, $database->createDocuments($colName, $docs)); - - // Any extra attributes should be allowed (fully schemaless) - $docs = [ - new Document(['$id' => 'doc11', 'title' => 'doc1', '$permissions' => $permissions]), - new Document(['$id' => 'doc21', 'moviename' => 'doc2', 'moviedescription' => 'test', '$permissions' => $permissions]), - new Document(['$id' => 'doc31', '$permissions' => $permissions]), - ]; - - $createdDocs = $database->createDocuments($colName, $docs); - $this->assertEquals(3, $createdDocs); - - // Create a single document with extra attribute as well - $single = $database->createDocument($colName, new Document(['$id' => 'docS', 'extra' => 'yes', '$permissions' => $permissions])); - $this->assertEquals('docS', $single->getId()); - $this->assertEquals('yes', $single->getAttribute('extra')); - - $found = $database->find($colName); - $this->assertCount(7, $found); - $doc11 = $database->getDocument($colName, 'doc11'); - $this->assertEquals('doc1', $doc11->getAttribute('title')); - - $doc21 = $database->getDocument($colName, 'doc21'); - $this->assertEquals('doc2', $doc21->getAttribute('moviename')); - $this->assertEquals('test', $doc21->getAttribute('moviedescription')); - - $updated = $database->updateDocument($colName, 'doc31', new Document(['moviename' => 'updated'])) - ; - $this->assertEquals('updated', $updated->getAttribute('moviename')); - - $this->assertTrue($database->deleteDocument($colName, 'doc21')); - $deleted = $database->getDocument($colName, 'doc21'); - $this->assertTrue($deleted->isEmpty()); - $remaining = $database->find($colName); - $this->assertCount(6, $remaining); - - // Bulk update: set a new extra attribute on all remaining docs - $modified = $database->updateDocuments($colName, new Document(['bulkExtra' => 'yes'])); - $this->assertEquals(6, $modified); - $all = $database->find($colName); - foreach ($all as $doc) { - $this->assertEquals('yes', $doc->getAttribute('bulkExtra')); - } - - // Upsert: create new and update existing with extra attributes preserved - $upserts = [ - new Document(['$id' => 'docU1', 'extraU' => 1, '$permissions' => $permissions]), - new Document(['$id' => 'doc1', 'extraU' => 2, '$permissions' => $permissions]), - ]; - $countUpserts = $database->upsertDocuments($colName, $upserts); - $this->assertEquals(2, $countUpserts); - $docU1 = $database->getDocument($colName, 'docU1'); - $this->assertEquals(1, $docU1->getAttribute('extraU')); - $doc1AfterUpsert = $database->getDocument($colName, 'doc1'); - $this->assertEquals(2, $doc1AfterUpsert->getAttribute('extraU')); - - // Increase/Decrease numeric attribute: add numeric attribute and mutate it - $database->createAttribute($colName, 'counter', Database::VAR_INTEGER, 0, false, 0); - $docS = $database->getDocument($colName, 'docS'); - $this->assertEquals(0, $docS->getAttribute('counter')); - $docS = $database->increaseDocumentAttribute($colName, 'docS', 'counter', 5); - $this->assertEquals(5, $docS->getAttribute('counter')); - $docS = $database->decreaseDocumentAttribute($colName, 'docS', 'counter', 3); - $this->assertEquals(2, $docS->getAttribute('counter')); - - $deletedByCounter = $database->deleteDocuments($colName, [Query::equal('counter', [2])]); - $this->assertEquals(1, $deletedByCounter); - - $deletedCount = $database->deleteDocuments($colName, [Query::startsWith('$id', 'doc')]); - $this->assertEquals(6, $deletedCount); - $postDelete = $database->find($colName); - $this->assertCount(0, $postDelete); - - $database->deleteCollection($colName); - } - public function testDecodeWithDifferentSelectionTypes(): void { /** @var Database $database */ @@ -6274,7 +6125,6 @@ public function testDecodeWithDifferentSelectionTypes(): void return; } - if (!$database->getAdapter()->getSupportForSpatialAttributes()) { $this->expectNotToPerformAssertions(); return; @@ -6432,45 +6282,6 @@ function (mixed $value) { $database->deleteCollection($storesId); } - public function testSchemalessDocumentInvalidInteralAttributeValidation(): void - { - /** @var Database $database */ - $database = static::getDatabase(); - - // test to ensure internal attributes are checked during creating schemaless document - if ($database->getAdapter()->getSupportForAttributes()) { - $this->expectNotToPerformAssertions(); - return; - } - - $colName = uniqid("schemaless"); - $database->createCollection($colName); - try { - $docs = [ - new Document(['$id' => true, 'freeA' => 'doc1']), - new Document(['$id' => true, 'freeB' => 'test']), - new Document(['$id' => true]), - ]; - $database->createDocuments($colName, $docs); - } catch (\Throwable $e) { - $this->assertInstanceOf(StructureException::class, $e); - } - - try { - $docs = [ - new Document(['$createdAt' => true, 'freeA' => 'doc1']), - new Document(['$updatedAt' => true, 'freeB' => 'test']), - new Document(['$permissions' => 12]), - ]; - $database->createDocuments($colName, $docs); - } catch (\Throwable $e) { - $this->assertInstanceOf(StructureException::class, $e); - } - - $database->deleteCollection($colName); - - } - public function testDecodeWithoutRelationships(): void { /** @var Database $database */ @@ -6481,7 +6292,6 @@ public function testDecodeWithoutRelationships(): void return; } - $database->addFilter( 'encryptTest', function (mixed $value) { @@ -6573,42 +6383,11 @@ function (mixed $value) { $database->deleteCollection($collectionId); } - public function testSchemaEnforcedDocumentCreation(): void - { - /** @var Database $database */ - $database = static::getDatabase(); - - if (!$database->getAdapter()->getSupportForAttributes()) { - $this->expectNotToPerformAssertions(); - return; - } - - $colName = uniqid("schema"); - $database->createCollection($colName); - $database->createAttribute($colName, 'key', Database::VAR_STRING, 50, true); - $database->createAttribute($colName, 'value', Database::VAR_STRING, 50, false, 'value'); - - $permissions = [Permission::read(Role::any()), Permission::write(Role::any()), Permission::update(Role::any())]; - - // Extra attributes should fail - $docs = [ - new Document(['$id' => 'doc11', 'key' => 'doc1', 'title' => 'doc1', '$permissions' => $permissions]), - new Document(['$id' => 'doc21', 'key' => 'doc2', 'moviename' => 'doc2', 'moviedescription' => 'test', '$permissions' => $permissions]), - new Document(['$id' => 'doc31', 'key' => 'doc3', '$permissions' => $permissions]), - ]; - - $this->expectException(StructureException::class); - $database->createDocuments($colName, $docs); - - $database->deleteCollection($colName); - } - public function testDecodeWithMultipleFilters(): void { /** @var Database $database */ $database = static::getDatabase(); - $database->addFilter( 'upperCase', function (mixed $value) { return strtoupper($value); }, diff --git a/tests/e2e/Adapter/Scopes/GeneralTests.php b/tests/e2e/Adapter/Scopes/GeneralTests.php index 31bdf6db3..b4bab2067 100644 --- a/tests/e2e/Adapter/Scopes/GeneralTests.php +++ b/tests/e2e/Adapter/Scopes/GeneralTests.php @@ -95,10 +95,6 @@ public function testPreserveDatesUpdate(): void /** @var Database $database */ $database = static::getDatabase(); - if (!$database->getAdapter()->getSupportForAttributes()) { - $this->expectNotToPerformAssertions(); - return; - } $database->setPreserveDates(true); @@ -194,10 +190,6 @@ public function testPreserveDatesCreate(): void /** @var Database $database */ $database = static::getDatabase(); - if (!$database->getAdapter()->getSupportForAttributes()) { - $this->expectNotToPerformAssertions(); - return; - } $database->setPreserveDates(true); diff --git a/tests/e2e/Adapter/Scopes/IndexTests.php b/tests/e2e/Adapter/Scopes/IndexTests.php index 54a5f9df6..ac8b11da7 100644 --- a/tests/e2e/Adapter/Scopes/IndexTests.php +++ b/tests/e2e/Adapter/Scopes/IndexTests.php @@ -164,16 +164,9 @@ public function testIndexValidation(): void $validator = new Index( $attributes, - $indexes, $database->getAdapter()->getMaxIndexLength(), $database->getAdapter()->getInternalIndexesKeys(), - $database->getAdapter()->getSupportForIndexArray(), - $database->getAdapter()->getSupportForSpatialAttributes(), - $database->getAdapter()->getSupportForSpatialIndexNull(), - $database->getAdapter()->getSupportForSpatialIndexOrder(), - $database->getAdapter()->getSupportForAttributes(), - $database->getAdapter()->getSupportForMultipleFulltextIndexes(), - $database->getAdapter()->getSupportForIdenticalIndexes() + $database->getAdapter()->getSupportForIndexArray() ); $errorMessage = 'Index length 701 is larger than the size for title1: 700"'; @@ -246,33 +239,19 @@ public function testIndexValidation(): void $validator = new Index( $attributes, - $indexes, $database->getAdapter()->getMaxIndexLength(), $database->getAdapter()->getInternalIndexesKeys(), - $database->getAdapter()->getSupportForIndexArray(), - $database->getAdapter()->getSupportForSpatialAttributes(), - $database->getAdapter()->getSupportForSpatialIndexNull(), - $database->getAdapter()->getSupportForSpatialIndexOrder(), - $database->getAdapter()->getSupportForAttributes(), - $database->getAdapter()->getSupportForMultipleFulltextIndexes(), - $database->getAdapter()->getSupportForIdenticalIndexes() + $database->getAdapter()->getSupportForIndexArray() ); - + $errorMessage = 'Attribute "integer" cannot be part of a FULLTEXT index, must be of type string'; $this->assertFalse($validator->isValid($indexes[0])); - - if ($database->getAdapter()->getSupportForAttributes()) { - $this->assertEquals('Attribute "integer" cannot be part of a fulltext index, must be of type string', $validator->getDescription()); - } elseif (!$database->getAdapter()->getSupportForMultipleFulltextIndexes()) { - $this->assertEquals('There is already a fulltext index in the collection', $validator->getDescription()); - } + $this->assertEquals($errorMessage, $validator->getDescription()); try { $database->createCollection($collection->getId(), $attributes, $indexes); - if ($database->getAdapter()->getSupportForAttributes()) { - $this->fail('Failed to throw exception'); - } + $this->fail('Failed to throw exception'); } catch (Exception $e) { - $this->assertEquals('Attribute "integer" cannot be part of a fulltext index, must be of type string', $e->getMessage()); + $this->assertEquals($errorMessage, $e->getMessage()); } @@ -326,7 +305,7 @@ public function testIndexLengthZero(): void $database->createCollection(__FUNCTION__); - $database->createAttribute(__FUNCTION__, 'title1', Database::VAR_STRING, $database->getAdapter()->getMaxIndexLength() + 300, true); + $database->createAttribute(__FUNCTION__, 'title1', Database::VAR_STRING, 1000, true); try { $database->createIndex(__FUNCTION__, 'index_title1', Database::INDEX_KEY, ['title1'], [0]); @@ -340,7 +319,7 @@ public function testIndexLengthZero(): void $database->createIndex(__FUNCTION__, 'index_title2', Database::INDEX_KEY, ['title2'], [0]); try { - $database->updateAttribute(__FUNCTION__, 'title2', Database::VAR_STRING, $database->getAdapter()->getMaxIndexLength() + 300, true); + $database->updateAttribute(__FUNCTION__, 'title2', Database::VAR_STRING, 1000, true); $this->fail('Failed to throw exception'); } catch (Throwable $e) { $this->assertEquals('Index length is longer than the maximum: '.$database->getAdapter()->getMaxIndexLength(), $e->getMessage()); @@ -506,129 +485,4 @@ public function testEmptySearch(): void ]); $this->assertEquals(0, count($documents)); } - - public function testMultipleFulltextIndexValidation(): void - { - - $fulltextSupport = $this->getDatabase()->getAdapter()->getSupportForFulltextIndex(); - if (!$fulltextSupport) { - $this->expectNotToPerformAssertions(); - return; - } - - /** @var Database $database */ - $database = static::getDatabase(); - - $collectionId = 'multiple_fulltext_test'; - try { - $database->createCollection($collectionId); - - $database->createAttribute($collectionId, 'title', Database::VAR_STRING, 256, false); - $database->createAttribute($collectionId, 'content', Database::VAR_STRING, 256, false); - $database->createIndex($collectionId, 'fulltext_title', Database::INDEX_FULLTEXT, ['title']); - - $supportsMultipleFulltext = $database->getAdapter()->getSupportForMultipleFulltextIndexes(); - - // Try to add second fulltext index - try { - $database->createIndex($collectionId, 'fulltext_content', Database::INDEX_FULLTEXT, ['content']); - - if ($supportsMultipleFulltext) { - $this->assertTrue(true, 'Multiple fulltext indexes are supported and second index was created successfully'); - } else { - $this->fail('Expected exception when creating second fulltext index, but none was thrown'); - } - } catch (Throwable $e) { - if (!$supportsMultipleFulltext) { - $this->assertTrue(true, 'Multiple fulltext indexes are not supported and exception was thrown as expected'); - } else { - $this->fail('Unexpected exception when creating second fulltext index: ' . $e->getMessage()); - } - } - - } finally { - // Clean up - $database->deleteCollection($collectionId); - } - } - - public function testIdenticalIndexValidation(): void - { - /** @var Database $database */ - $database = static::getDatabase(); - - $collectionId = 'identical_index_test'; - - try { - $database->createCollection($collectionId); - - $database->createAttribute($collectionId, 'name', Database::VAR_STRING, 256, false); - $database->createAttribute($collectionId, 'age', Database::VAR_INTEGER, 8, false); - - $database->createIndex($collectionId, 'index1', Database::INDEX_KEY, ['name', 'age'], [], [Database::ORDER_ASC, Database::ORDER_DESC]); - - $supportsIdenticalIndexes = $database->getAdapter()->getSupportForIdenticalIndexes(); - - // Try to add identical index (failure) - try { - $database->createIndex($collectionId, 'index2', Database::INDEX_KEY, ['name', 'age'], [], [Database::ORDER_ASC, Database::ORDER_DESC]); - if ($supportsIdenticalIndexes) { - $this->assertTrue(true, 'Identical indexes are supported and second index was created successfully'); - } else { - $this->fail('Expected exception but got none'); - } - - } catch (Throwable $e) { - if (!$supportsIdenticalIndexes) { - $this->assertTrue(true, 'Identical indexes are not supported and exception was thrown as expected'); - } else { - $this->fail('Unexpected exception when creating identical index: ' . $e->getMessage()); - } - - } - - // Test with different attributes order - faliure - try { - $database->createIndex($collectionId, 'index3', Database::INDEX_KEY, ['age', 'name'], [], [ Database::ORDER_ASC, Database::ORDER_DESC]); - $this->assertTrue(true, 'Index with different attributes was created successfully'); - } catch (Throwable $e) { - if (!$supportsIdenticalIndexes) { - $this->assertTrue(true, 'Identical indexes are not supported and exception was thrown as expected'); - } else { - $this->fail('Unexpected exception when creating identical index: ' . $e->getMessage()); - } - } - - // Test with different orders order - faliure - try { - $database->createIndex($collectionId, 'index4', Database::INDEX_KEY, ['age', 'name'], [], [ Database::ORDER_DESC, Database::ORDER_ASC]); - $this->assertTrue(true, 'Index with different attributes was created successfully'); - } catch (Throwable $e) { - if (!$supportsIdenticalIndexes) { - $this->assertTrue(true, 'Identical indexes are not supported and exception was thrown as expected'); - } else { - $this->fail('Unexpected exception when creating identical index: ' . $e->getMessage()); - } - } - - // Test with different attributes - success - try { - $database->createIndex($collectionId, 'index5', Database::INDEX_KEY, ['name'], [], [Database::ORDER_ASC]); - $this->assertTrue(true, 'Index with different attributes was created successfully'); - } catch (Throwable $e) { - $this->fail('Unexpected exception when creating index with different attributes: ' . $e->getMessage()); - } - - // Test with different orders - success - try { - $database->createIndex($collectionId, 'index6', Database::INDEX_KEY, ['name', 'age'], [], [Database::ORDER_ASC]); - $this->assertTrue(true, 'Index with different orders was created successfully'); - } catch (Throwable $e) { - $this->fail('Unexpected exception when creating index with different orders: ' . $e->getMessage()); - } - } finally { - // Clean up - $database->deleteCollection($collectionId); - } - } } diff --git a/tests/e2e/Adapter/Scopes/PermissionTests.php b/tests/e2e/Adapter/Scopes/PermissionTests.php index 5a2233312..50e14c90c 100644 --- a/tests/e2e/Adapter/Scopes/PermissionTests.php +++ b/tests/e2e/Adapter/Scopes/PermissionTests.php @@ -943,11 +943,6 @@ public function testCollectionPermissionsRelationshipsFindWorks(array $data): vo /** @var Database $database */ $database = static::getDatabase(); - if (!$database->getAdapter()->getSupportForRelationships()) { - $this->expectNotToPerformAssertions(); - return; - } - $documents = $database->find( $collection->getId() ); @@ -1025,11 +1020,6 @@ public function testCollectionPermissionsRelationshipsGetWorks(array $data): arr /** @var Database $database */ $database = static::getDatabase(); - if (!$database->getAdapter()->getSupportForRelationships()) { - $this->expectNotToPerformAssertions(); - return []; - } - $document = $database->getDocument( $collection->getId(), $document->getId() diff --git a/tests/e2e/Adapter/Scopes/SpatialTests.php b/tests/e2e/Adapter/Scopes/SpatialTests.php index 15fb45b16..31093e724 100644 --- a/tests/e2e/Adapter/Scopes/SpatialTests.php +++ b/tests/e2e/Adapter/Scopes/SpatialTests.php @@ -21,8 +21,7 @@ public function testSpatialCollection(): void $database = static::getDatabase(); $collectionName = "test_spatial_Col"; if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); }; $attributes = [ new Document([ @@ -95,8 +94,7 @@ public function testSpatialTypeDocuments(): void /** @var Database $database */ $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); } $collectionName = 'test_spatial_doc_'; @@ -920,8 +918,7 @@ public function testComplexGeometricShapes(): void /** @var Database $database */ $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); } $collectionName = 'complex_shapes_'; @@ -1351,8 +1348,7 @@ public function testSpatialQueryCombinations(): void /** @var Database $database */ $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); } $collectionName = 'spatial_combinations_'; @@ -1482,8 +1478,7 @@ public function testSpatialBulkOperation(): void /** @var Database $database */ $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); } $collectionName = 'test_spatial_bulk_ops'; @@ -1785,8 +1780,7 @@ public function testSptialAggregation(): void /** @var Database $database */ $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); } $collectionName = 'spatial_agg_'; try { @@ -1873,8 +1867,7 @@ public function testUpdateSpatialAttributes(): void /** @var Database $database */ $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); } $collectionName = 'spatial_update_attrs_'; @@ -1960,8 +1953,7 @@ public function testSpatialAttributeDefaults(): void /** @var Database $database */ $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); } $collectionName = 'spatial_defaults_'; @@ -2065,8 +2057,7 @@ public function testInvalidSpatialTypes(): void /** @var Database $database */ $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); } $collectionName = 'test_invalid_spatial_types'; @@ -2171,8 +2162,7 @@ public function testSpatialDistanceInMeter(): void /** @var Database $database */ $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); } $collectionName = 'spatial_distance_meters_'; @@ -2242,13 +2232,11 @@ public function testSpatialDistanceInMeterForMultiDimensionGeometry(): void /** @var Database $database */ $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); } if (!$database->getAdapter()->getSupportForDistanceBetweenMultiDimensionGeometryInMeters()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial distance(in meter) for multidimension'); } $multiCollection = 'spatial_distance_meters_multi_'; @@ -2374,13 +2362,11 @@ public function testSpatialDistanceInMeterError(): void /** @var Database $database */ $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); } if ($database->getAdapter()->getSupportForDistanceBetweenMultiDimensionGeometryInMeters()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter supports spatial distance (in meter) for multidimension geometries'); } $collection = 'spatial_distance_error_test'; @@ -2459,8 +2445,7 @@ public function testSpatialEncodeDecode(): void /** @var Database $database */ $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); } $point = "POINT(1 2)"; $line = "LINESTRING(1 2, 1 2)"; @@ -2650,8 +2635,7 @@ public function testSpatialDocOrder(): void /** @var Database $database */ $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); } $collectionName = 'test_spatial_order_axis'; @@ -2682,8 +2666,7 @@ public function testInvalidCoordinateDocuments(): void /** @var Database $database */ $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSpatialAttributes()) { - $this->expectNotToPerformAssertions(); - return; + $this->markTestSkipped('Adapter does not support spatial attributes'); } $collectionName = 'test_invalid_coord_'; diff --git a/tests/e2e/Adapter/SharedTables/MongoDBTest.php b/tests/e2e/Adapter/SharedTables/MongoDBTest.php deleted file mode 100644 index 9a9f2e749..000000000 --- a/tests/e2e/Adapter/SharedTables/MongoDBTest.php +++ /dev/null @@ -1,111 +0,0 @@ -connect('redis', 6379); - $redis->flushAll(); - $cache = new Cache(new RedisAdapter($redis)); - - $schema = 'utopiaTests'; // same as $this->testDatabase - $client = new Client( - $schema, - 'mongo', - 27017, - 'root', - 'password', - false - ); - - $database = new Database(new Mongo($client), $cache); - $database - ->setDatabase($schema) - ->setSharedTables(true) - ->setTenant(999) - ->setNamespace(static::$namespace = 'my_shared_tables'); - - if ($database->exists()) { - $database->delete(); - } - - $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); - } - - protected static function deleteColumn(string $collection, string $column): bool - { - return true; - } - - protected static function deleteIndex(string $collection, string $index): bool - { - return true; - } -} diff --git a/tests/resources/mongo/entrypoint.sh b/tests/resources/mongo/entrypoint.sh deleted file mode 100755 index 8119a4fa7..000000000 --- a/tests/resources/mongo/entrypoint.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -set -e - -# Fix keyfile permissions -if [ -f "/tmp/keyfile" ]; then - cp /tmp/keyfile /etc/mongo-keyfile - chmod 400 /etc/mongo-keyfile - chown mongodb:mongodb /etc/mongo-keyfile -fi - -# Use MongoDB's standard entrypoint with our command -exec docker-entrypoint.sh mongod --replSet rs0 --bind_ip_all --auth --keyFile /etc/mongo-keyfile \ No newline at end of file diff --git a/tests/resources/mongo/mongo-keyfile b/tests/resources/mongo/mongo-keyfile deleted file mode 100644 index 5585939eb..000000000 --- a/tests/resources/mongo/mongo-keyfile +++ /dev/null @@ -1,16 +0,0 @@ -ydIuYSvU/9QLt7fkH32IdXbP2z2+w+fzSEoolW8Q1Z8nLhRyrZF0Zq7a0KzeNI7K -gPIl1ikI6ob6h0+RxYmGeOOUjjkcBlkvYrmABDKsRipTkTTp4z0fUBTIUJV0lVvs -N9+VpM0/pLLIhI8jb38aa7pmsoufBQ3uiNR68ZFykPqzZQ4d5VfMqfZk7z3dpFlh -DURPOOG0HAFe68MLXVFYdaHGW4yomuTPrpzWSiUhFAPFEBYg4elARQc4CaiinFds -SQi/SrUsYMGODPr+on9/lboia/SInaSP+dzDqpsbL29atvIVHtU29RlPJdZ2V1ub -Oe2O1xN9F59TtjNUgDiAtMGKTMS/0S1mbPC6Og5JAR7U4xZ7/6S5n3+p0RjYyTlH -fhssJ7pc/bveN6mShNrsIKK0Z50YYjablzm07EDJYhfEWMG5Wu1AvEVqEH68ioDl -JL5QO63A2bXvMN7dXS69+E0hHn6xaZYu+CnKedvgWdyhraCT1Q01ZyDyv2y7isGD -1BAlNLlt+cPMCitETcxZne+JHdkL/mDKffHUPM4Drtzchg4DbiG49uC9Ib7zTws+ -NcburXY+9B8j7WN7ZHXhiB7/OWJ/IHJCZTdKz70mEPH4AHoRFpZNM5eMnYxYdbQD -40MhAS7fuOYhtFIQiQ+SCeFMucE3KYvp1JpTVQwT4SNrIlHPqfPn5xFBcgDjhvwT -hHJCgXP4HrRuf47Ta6kHy2UFQ7r5JOqSZSOFwP+tUyfhjEB5ZWJ1qCUZxFagoc9A -//9SoyulZwCxEr2ijmes1Nzv56hSTjYb6pPjFWd92G87w+VZv4R/vF5nwcYUyuIS -iQWPs/kOzb4NeJW24lNzR2zH2BsJt3OI+BFY64cc8O0o6EtFWcoabwyJYKe6RXPX -0S4ngcnGzRP+tVa6LsrjAYrNpmZDrP9x93pXQHfByTS2oSaI1eGeAagFTu/HS2kC -uCJ0HfH99sRSgJ1Ab+2C8G8305meDAbtdCtvl/1anPnV6ISy diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index 7f443ab5d..b9b261dc0 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -86,6 +86,7 @@ public function testCreate(): void $this->assertEquals('title', $query->getAttribute()); $this->assertEquals([], $query->getValues()); + // Test new NOT query types $query = Query::notContains('tags', ['test', 'example']); $this->assertEquals(Query::TYPE_NOT_CONTAINS, $query->getMethod()); @@ -211,6 +212,7 @@ public function testParse(): void $this->assertEquals('score', $query->getAttribute()); $this->assertEquals(8.5, $query->getValues()[0]); + // Test new NOT query types parsing $query = Query::parse(Query::notContains('tags', ['unwanted', 'spam'])->toString()); $this->assertEquals('notContains', $query->getMethod()); $this->assertEquals('tags', $query->getAttribute()); @@ -443,6 +445,7 @@ public function testIsMethod(): void public function testNewQueryTypesInTypesArray(): void { + // Test that all new query types are included in the TYPES array $this->assertContains(Query::TYPE_NOT_CONTAINS, Query::TYPES); $this->assertContains(Query::TYPE_NOT_SEARCH, Query::TYPES); $this->assertContains(Query::TYPE_NOT_STARTS_WITH, Query::TYPES); diff --git a/tests/unit/Validator/IndexTest.php b/tests/unit/Validator/IndexTest.php index 9e544c6a6..a2862830c 100644 --- a/tests/unit/Validator/IndexTest.php +++ b/tests/unit/Validator/IndexTest.php @@ -51,7 +51,7 @@ public function testAttributeNotFound(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), $collection->getAttribute('indexes'), 768); + $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); $this->assertEquals('Invalid index attribute "not_exist" not found', $validator->getDescription()); @@ -100,10 +100,10 @@ public function testFulltextWithNonString(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), $collection->getAttribute('indexes'), 768); + $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); - $this->assertEquals('Attribute "date" cannot be part of a fulltext index, must be of type string', $validator->getDescription()); + $this->assertEquals('Attribute "date" cannot be part of a FULLTEXT index, must be of type string', $validator->getDescription()); } /** @@ -138,7 +138,7 @@ public function testIndexLength(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), $collection->getAttribute('indexes'), 768); + $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); $this->assertEquals('Index length is longer than the maximum: 768', $validator->getDescription()); @@ -185,7 +185,7 @@ public function testMultipleIndexLength(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), $collection->getAttribute('indexes'), 768); + $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; $this->assertTrue($validator->isValid($index)); @@ -232,7 +232,7 @@ public function testEmptyAttributes(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), $collection->getAttribute('indexes'), 768); + $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); $this->assertEquals('No attributes provided for index', $validator->getDescription()); @@ -270,7 +270,7 @@ public function testDuplicatedAttributes(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), $collection->getAttribute('indexes'), 768); + $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); $this->assertEquals('Duplicate attributes provided', $validator->getDescription()); @@ -308,7 +308,7 @@ public function testDuplicatedAttributesDifferentOrder(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), $collection->getAttribute('indexes'), 768); + $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); } @@ -345,7 +345,7 @@ public function testReservedIndexKey(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), $collection->getAttribute('indexes'), 768, ['PRIMARY']); + $validator = new Index($collection->getAttribute('attributes'), 768, ['PRIMARY']); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); } diff --git a/tests/unit/Validator/KeyTest.php b/tests/unit/Validator/KeyTest.php index e09ef402e..ca85ae56b 100644 --- a/tests/unit/Validator/KeyTest.php +++ b/tests/unit/Validator/KeyTest.php @@ -66,9 +66,11 @@ public function testValues(): void $this->assertEquals(false, $this->object->isValid('as+5dasdasdas')); $this->assertEquals(false, $this->object->isValid('as=5dasdasdas')); - // At most 255 chars - $this->assertEquals(true, $this->object->isValid(str_repeat('a', 255))); - $this->assertEquals(false, $this->object->isValid(str_repeat('a', 256))); + // At most 36 chars + $this->assertEquals(true, $this->object->isValid('socialAccountForYoutubeSubscribersss')); + $this->assertEquals(false, $this->object->isValid('socialAccountForYoutubeSubscriberssss')); + $this->assertEquals(true, $this->object->isValid('5f058a89258075f058a89258075f058t9214')); + $this->assertEquals(false, $this->object->isValid('5f058a89258075f058a89258075f058tx9214')); // Internal keys $validator = new Key(true); diff --git a/tests/unit/Validator/LabelTest.php b/tests/unit/Validator/LabelTest.php index 3d9bf7576..c3eef2fb4 100644 --- a/tests/unit/Validator/LabelTest.php +++ b/tests/unit/Validator/LabelTest.php @@ -58,8 +58,10 @@ public function testValues(): void $this->assertEquals(false, $this->object->isValid('as+5dasdasdas')); $this->assertEquals(false, $this->object->isValid('as=5dasdasdas')); - // At most 255 chars - $this->assertEquals(true, $this->object->isValid(str_repeat('a', 255))); - $this->assertEquals(false, $this->object->isValid(str_repeat('a', 256))); + // At most 36 chars + $this->assertEquals(true, $this->object->isValid('socialAccountForYoutubeSubscribersss')); + $this->assertEquals(false, $this->object->isValid('socialAccountForYoutubeSubscriberssss')); + $this->assertEquals(true, $this->object->isValid('5f058a89258075f058a89258075f058t9214')); + $this->assertEquals(false, $this->object->isValid('5f058a89258075f058a89258075f058tx9214')); } } diff --git a/tests/unit/Validator/PermissionsTest.php b/tests/unit/Validator/PermissionsTest.php index 505e69dec..bc03fb201 100644 --- a/tests/unit/Validator/PermissionsTest.php +++ b/tests/unit/Validator/PermissionsTest.php @@ -248,25 +248,24 @@ public function testInvalidPermissions(): void // team:$value, member:$value and user:$value must have valid Key for $value // No leading special chars $this->assertFalse($object->isValid([Permission::read(Role::user('_1234'))])); - $this->assertEquals('Role "user" identifier value is invalid: Parameter must contain at most 255 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); + $this->assertEquals('Role "user" identifier value is invalid: Parameter must contain at most 36 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); $this->assertFalse($object->isValid([Permission::read(Role::team('-1234'))])); - $this->assertEquals('Role "team" identifier value is invalid: Parameter must contain at most 255 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); + $this->assertEquals('Role "team" identifier value is invalid: Parameter must contain at most 36 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); $this->assertFalse($object->isValid([Permission::read(Role::member('.1234'))])); - $this->assertEquals('Role "member" identifier value is invalid: Parameter must contain at most 255 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); + $this->assertEquals('Role "member" identifier value is invalid: Parameter must contain at most 36 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); // No unsupported special characters $this->assertFalse($object->isValid([Permission::read(Role::user('12$4'))])); - $this->assertEquals('Role "user" identifier value is invalid: Parameter must contain at most 255 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); + $this->assertEquals('Role "user" identifier value is invalid: Parameter must contain at most 36 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); $this->assertFalse($object->isValid([Permission::read(Role::user('12&4'))])); - $this->assertEquals('Role "user" identifier value is invalid: Parameter must contain at most 255 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); + $this->assertEquals('Role "user" identifier value is invalid: Parameter must contain at most 36 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); $this->assertFalse($object->isValid([Permission::read(Role::user('ab(124'))])); - $this->assertEquals('Role "user" identifier value is invalid: Parameter must contain at most 255 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); + $this->assertEquals('Role "user" identifier value is invalid: Parameter must contain at most 36 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); - // Shorter than 255 chars - - $this->assertTrue($object->isValid([Permission::read(Role::user(ID::custom(str_repeat('a', 255))))])); - $this->assertFalse($object->isValid([Permission::read(Role::user(ID::custom(str_repeat('a', 256))))])); - $this->assertEquals('Role "user" identifier value is invalid: Parameter must contain at most 255 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); + // Shorter than 36 chars + $this->assertTrue($object->isValid([Permission::read(Role::user(ID::custom('aaaaaaaabbbbbbbbccccccccddddddddeeee')))])); + $this->assertFalse($object->isValid([Permission::read(Role::user(ID::custom('aaaaaaaabbbbbbbbccccccccddddddddeeeee')))])); + $this->assertEquals('Role "user" identifier value is invalid: Parameter must contain at most 36 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); // Permission role must begin with one of: member, role, team, user $this->assertFalse($object->isValid(['update("memmber:1234")'])); @@ -278,7 +277,7 @@ public function testInvalidPermissions(): void // Team permission $this->assertFalse($object->isValid([Permission::read(Role::team(ID::custom('_abcd')))])); - $this->assertEquals('Role "team" identifier value is invalid: Parameter must contain at most 255 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); + $this->assertEquals('Role "team" identifier value is invalid: Parameter must contain at most 36 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); $this->assertFalse($object->isValid([Permission::read(Role::team(ID::custom('abcd/')))])); $this->assertEquals('Dimension must not be empty', $object->getDescription()); $this->assertFalse($object->isValid([Permission::read(Role::team(ID::custom(''), 'abcd'))])); @@ -288,9 +287,9 @@ public function testInvalidPermissions(): void $this->assertFalse($object->isValid([Permission::read(Role::team(ID::custom('abcd'), 'e/fgh'))])); $this->assertEquals('Only one dimension can be provided', $object->getDescription()); $this->assertFalse($object->isValid([Permission::read(Role::team(ID::custom('ab&cd3'), 'efgh'))])); - $this->assertEquals('Role "team" identifier value is invalid: Parameter must contain at most 255 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); + $this->assertEquals('Role "team" identifier value is invalid: Parameter must contain at most 36 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); $this->assertFalse($object->isValid([Permission::read(Role::team(ID::custom('abcd'), 'ef*gh'))])); - $this->assertEquals('Role "team" dimension value is invalid: Parameter must contain at most 255 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); + $this->assertEquals('Role "team" dimension value is invalid: Parameter must contain at most 36 chars. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char', $object->getDescription()); // Permission-list length must be valid $object = new Permissions(100); diff --git a/tests/unit/Validator/StructureTest.php b/tests/unit/Validator/StructureTest.php index a0b448ff5..68fa73bf8 100644 --- a/tests/unit/Validator/StructureTest.php +++ b/tests/unit/Validator/StructureTest.php @@ -716,7 +716,7 @@ public function testId(): void ); $sqlId = '1000'; - $mongoId = '0198fffb-d664-710a-9765-f922b3e81e3d'; + $mongoId = '507f1f77bcf86cd799439011'; $this->assertEquals(true, $validator->isValid(new Document([ '$collection' => ID::custom('posts'), @@ -748,7 +748,7 @@ public function testId(): void $validator = new Structure( new Document($this->collection), - Database::VAR_UUID7 + Database::VAR_OBJECT_ID ); $this->assertEquals(true, $validator->isValid(new Document([