diff --git a/composer.lock b/composer.lock index 774cd790d..2a5302cd2 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "brick/math", - "version": "0.12.3", + "version": "0.13.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", - "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", + "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", "shasum": "" }, "require": { @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.3" + "source": "https://github.com/brick/math/tree/0.13.1" }, "funding": [ { @@ -64,7 +64,7 @@ "type": "github" } ], - "time": "2025-02-28T13:11:00+00:00" + "time": "2025-03-29T13:50:30+00:00" }, { "name": "composer/semver", @@ -149,16 +149,16 @@ }, { "name": "google/protobuf", - "version": "v4.30.2", + "version": "v4.31.1", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "a4c4d8565b40b9f76debc9dfeb221412eacb8ced" + "reference": "2b028ce8876254e2acbeceea7d9b573faad41864" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/a4c4d8565b40b9f76debc9dfeb221412eacb8ced", - "reference": "a4c4d8565b40b9f76debc9dfeb221412eacb8ced", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/2b028ce8876254e2acbeceea7d9b573faad41864", + "reference": "2b028ce8876254e2acbeceea7d9b573faad41864", "shasum": "" }, "require": { @@ -187,9 +187,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.30.2" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.31.1" }, - "time": "2025-03-26T18:01:50+00:00" + "time": "2025-05-28T18:52:35+00:00" }, { "name": "nyholm/psr7", @@ -337,16 +337,16 @@ }, { "name": "open-telemetry/api", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "4e3bb38e069876fb73c2ce85c89583bf2b28cd86" + "reference": "b3a9286f9c1c8247c83493c5b1fa475cd0cec7f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/4e3bb38e069876fb73c2ce85c89583bf2b28cd86", - "reference": "4e3bb38e069876fb73c2ce85c89583bf2b28cd86", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/b3a9286f9c1c8247c83493c5b1fa475cd0cec7f7", + "reference": "b3a9286f9c1c8247c83493c5b1fa475cd0cec7f7", "shasum": "" }, "require": { @@ -366,7 +366,7 @@ ] }, "branch-alias": { - "dev-main": "1.1.x-dev" + "dev-main": "1.4.x-dev" } }, "autoload": { @@ -403,7 +403,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-05-07T12:32:21+00:00" + "time": "2025-06-19T23:36:51+00:00" }, { "name": "open-telemetry/context", @@ -466,16 +466,16 @@ }, { "name": "open-telemetry/exporter-otlp", - "version": "1.3.0", + "version": "1.3.2", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/exporter-otlp.git", - "reference": "19adf03d2b0f91f9e9b1c7f93db6c755c737cf6c" + "reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/19adf03d2b0f91f9e9b1c7f93db6c755c737cf6c", - "reference": "19adf03d2b0f91f9e9b1c7f93db6c755c737cf6c", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/196f3a1dbce3b2c0f8110d164232c11ac00ddbb2", + "reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2", "shasum": "" }, "require": { @@ -526,7 +526,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-05-12T00:36:35+00:00" + "time": "2025-06-16T00:24:51+00:00" }, { "name": "open-telemetry/gen-otlp-protobuf", @@ -593,22 +593,22 @@ }, { "name": "open-telemetry/sdk", - "version": "1.4.0", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "939d3a28395c249a763676458140dad44b3a8011" + "reference": "1c0371794e4c0700afd4a9d4d8511cb5e3f78e6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/939d3a28395c249a763676458140dad44b3a8011", - "reference": "939d3a28395c249a763676458140dad44b3a8011", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/1c0371794e4c0700afd4a9d4d8511cb5e3f78e6a", + "reference": "1c0371794e4c0700afd4a9d4d8511cb5e3f78e6a", "shasum": "" }, "require": { "ext-json": "*", "nyholm/psr7-server": "^1.1", - "open-telemetry/api": "~1.0 || ~1.1", + "open-telemetry/api": "~1.4.0", "open-telemetry/context": "^1.0", "open-telemetry/sem-conv": "^1.0", "php": "^8.1", @@ -631,6 +631,10 @@ "type": "library", "extra": { "spi": { + "OpenTelemetry\\API\\Configuration\\ConfigEnv\\EnvComponentLoader": [ + "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderHttpConfig", + "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderPeerConfig" + ], "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" ] @@ -679,20 +683,20 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-05-07T12:32:21+00:00" + "time": "2025-06-19T23:36:51+00:00" }, { "name": "open-telemetry/sem-conv", - "version": "1.32.0", + "version": "1.32.1", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sem-conv.git", - "reference": "16585cc0dbc3032a318e274043454679430d2ebf" + "reference": "94daa85ea61a8e2b7e1b0af6be0e875bedda7c22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/16585cc0dbc3032a318e274043454679430d2ebf", - "reference": "16585cc0dbc3032a318e274043454679430d2ebf", + "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/94daa85ea61a8e2b7e1b0af6be0e875bedda7c22", + "reference": "94daa85ea61a8e2b7e1b0af6be0e875bedda7c22", "shasum": "" }, "require": { @@ -736,7 +740,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-05-05T03:58:53+00:00" + "time": "2025-06-24T02:32:27+00:00" }, { "name": "php-http/discovery", @@ -1158,21 +1162,20 @@ }, { "name": "ramsey/uuid", - "version": "4.7.6", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "91039bc1faa45ba123c4328958e620d382ec7088" + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", - "reference": "91039bc1faa45ba123c4328958e620d382ec7088", + "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", - "ext-json": "*", + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -1180,26 +1183,23 @@ "rhumsaa/uuid": "self.version" }, "require-dev": { - "captainhook/captainhook": "^5.10", + "captainhook/captainhook": "^5.25", "captainhook/plugin-composer": "^5.3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "doctrine/annotations": "^1.8", - "ergebnis/composer-normalize": "^2.15", - "mockery/mockery": "^1.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", "paragonie/random-lib": "^2", - "php-mock/php-mock": "^2.2", - "php-mock/php-mock-mockery": "^1.3", - "php-parallel-lint/php-parallel-lint": "^1.1", - "phpbench/phpbench": "^1.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^8.5 || ^9", - "ramsey/composer-repl": "^1.4", - "slevomat/coding-standard": "^8.4", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^4.9" + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" }, "suggest": { "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", @@ -1234,32 +1234,22 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.7.6" + "source": "https://github.com/ramsey/uuid/tree/4.9.0" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", - "type": "tidelift" - } - ], - "time": "2024-04-27T21:32:50+00:00" + "time": "2025-06-25T14:20:11+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -1272,7 +1262,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -1297,7 +1287,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -1313,20 +1303,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/http-client", - "version": "v7.2.4", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6" + "reference": "4403d87a2c16f33345dca93407a8714ee8c05a64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", - "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "url": "https://api.github.com/repos/symfony/http-client/zipball/4403d87a2c16f33345dca93407a8714ee8c05a64", + "reference": "4403d87a2c16f33345dca93407a8714ee8c05a64", "shasum": "" }, "require": { @@ -1338,6 +1328,7 @@ }, "conflict": { "amphp/amp": "<2.5", + "amphp/socket": "<1.1", "php-http/discovery": "<1.15", "symfony/http-foundation": "<6.4" }, @@ -1350,7 +1341,6 @@ "require-dev": { "amphp/http-client": "^4.2.1|^5.0", "amphp/http-tunnel": "^1.0|^2.0", - "amphp/socket": "^1.1", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", @@ -1392,7 +1382,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.2.4" + "source": "https://github.com/symfony/http-client/tree/v7.3.1" }, "funding": [ { @@ -1408,20 +1398,20 @@ "type": "tidelift" } ], - "time": "2025-02-13T10:27:23+00:00" + "time": "2025-06-28T07:58:39+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.5.2", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" + "reference": "75d7043853a42837e68111812f4d964b01e5101c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", - "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", "shasum": "" }, "require": { @@ -1434,7 +1424,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -1470,7 +1460,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" }, "funding": [ { @@ -1486,7 +1476,7 @@ "type": "tidelift" } ], - "time": "2024-12-07T08:49:48+00:00" + "time": "2025-04-29T11:18:49+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -1647,16 +1637,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -1674,7 +1664,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -1710,7 +1700,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -1726,20 +1716,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "tbachert/spi", - "version": "v1.0.3", + "version": "v1.0.4", "source": { "type": "git", "url": "https://github.com/Nevay/spi.git", - "reference": "506a79c98e1a51522e76ee921ccb6c62d52faf3a" + "reference": "86e355edfdd57f9cb720bd2ac3af7dde521ca0e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nevay/spi/zipball/506a79c98e1a51522e76ee921ccb6c62d52faf3a", - "reference": "506a79c98e1a51522e76ee921ccb6c62d52faf3a", + "url": "https://api.github.com/repos/Nevay/spi/zipball/86e355edfdd57f9cb720bd2ac3af7dde521ca0e7", + "reference": "86e355edfdd57f9cb720bd2ac3af7dde521ca0e7", "shasum": "" }, "require": { @@ -1757,7 +1747,7 @@ "extra": { "class": "Nevay\\SPI\\Composer\\Plugin", "branch-alias": { - "dev-main": "0.2.x-dev" + "dev-main": "1.0.x-dev" }, "plugin-optional": true }, @@ -1776,9 +1766,9 @@ ], "support": { "issues": "https://github.com/Nevay/spi/issues", - "source": "https://github.com/Nevay/spi/tree/v1.0.3" + "source": "https://github.com/Nevay/spi/tree/v1.0.4" }, - "time": "2025-04-02T19:38:14+00:00" + "time": "2025-06-28T20:18:22+00:00" }, { "name": "utopia-php/cache", @@ -1880,16 +1870,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.19", + "version": "0.33.20", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "64c7b7bb8a8595ffe875fa8d4b7705684dbf46c0" + "reference": "e1c7ab4e0b5b0a9a70256b1e00912e101e76a131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/64c7b7bb8a8595ffe875fa8d4b7705684dbf46c0", - "reference": "64c7b7bb8a8595ffe875fa8d4b7705684dbf46c0", + "url": "https://api.github.com/repos/utopia-php/http/zipball/e1c7ab4e0b5b0a9a70256b1e00912e101e76a131", + "reference": "e1c7ab4e0b5b0a9a70256b1e00912e101e76a131", "shasum": "" }, "require": { @@ -1921,9 +1911,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.19" + "source": "https://github.com/utopia-php/http/tree/0.33.20" }, - "time": "2025-03-06T11:37:49+00:00" + "time": "2025-05-18T23:51:21+00:00" }, { "name": "utopia-php/pools", @@ -2498,16 +2488,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.25", + "version": "1.12.27", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "e310849a19e02b8bfcbb63147f495d8f872dd96f" + "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e310849a19e02b8bfcbb63147f495d8f872dd96f", - "reference": "e310849a19e02b8bfcbb63147f495d8f872dd96f", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3a6e423c076ab39dfedc307e2ac627ef579db162", + "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162", "shasum": "" }, "require": { @@ -2552,7 +2542,7 @@ "type": "github" } ], - "time": "2025-04-27T12:20:45+00:00" + "time": "2025-05-21T20:51:45+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4131,7 +4121,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -4139,6 +4129,6 @@ "ext-pdo": "*", "ext-mbstring": "*" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index b08b160d3..708926548 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -11,7 +11,6 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\NotFound as NotFoundException; -use Utopia\Database\Exception\Order as OrderException; use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Helpers\ID; @@ -1517,84 +1516,69 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $queries = array_map(fn ($query) => clone $query, $queries); - $hasIdAttribute = false; - foreach ($orderAttributes as $i => $attribute) { - $originalAttribute = $attribute; + $cursorWhere = []; - $attribute = $this->getInternalKeyForAttribute($attribute); + foreach ($orderAttributes as $i => $originalAttribute) { + $attribute = $this->getInternalKeyForAttribute($originalAttribute); $attribute = $this->filter($attribute); - if (\in_array($attribute, ['_uid', '_id'])) { - $hasIdAttribute = true; - } $orderType = $this->filter($orderTypes[$i] ?? Database::ORDER_ASC); + $direction = $orderType; - // Get most dominant/first order attribute - if ($i === 0 && !empty($cursor)) { - $orderMethodSequence = Query::TYPE_GREATER; // To preserve natural order - $orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; + if ($cursorDirection === Database::CURSOR_BEFORE) { + $direction = ($direction === Database::ORDER_ASC) + ? Database::ORDER_DESC + : Database::ORDER_ASC; + } - if ($cursorDirection === Database::CURSOR_BEFORE) { - $orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; - $orderMethodSequence = $orderType === Database::ORDER_ASC ? Query::TYPE_LESSER : Query::TYPE_GREATER; - $orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; - } + $orders[] = "{$this->quote($attribute)} {$direction}"; + + // Build pagination WHERE clause only if we have a cursor + if (!empty($cursor)) { + // Special case: No tie breaks. only 1 attribute and it's a unique primary key + if (count($orderAttributes) === 1 && $i === 0 && $originalAttribute === '$sequence') { + $operator = ($direction === Database::ORDER_DESC) + ? Query::TYPE_LESSER + : Query::TYPE_GREATER; + + $bindName = ":cursor_pk"; + $binds[$bindName] = $cursor[$originalAttribute]; - if (\is_null($cursor[$originalAttribute] ?? null)) { - throw new OrderException( - message: "Order attribute '{$originalAttribute}' is empty", - attribute: $originalAttribute - ); + $cursorWhere[] = "{$this->quote($alias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}"; + break; } - $binds[':cursor'] = $cursor[$originalAttribute]; - - $where[] = "( - {$this->quote($alias)}.{$this->quote($attribute)} {$this->getSQLOperator($orderMethod)} :cursor - OR ( - {$this->quote($alias)}.{$this->quote($attribute)} = :cursor - AND - {$this->quote($alias)}._id {$this->getSQLOperator($orderMethodSequence)} {$cursor['$sequence']} - ) - )"; - } elseif ($cursorDirection === Database::CURSOR_BEFORE) { - $orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; - } + $conditions = []; - $orders[] = "{$this->quote($attribute)} {$orderType}"; - } + // Add equality conditions for previous attributes + for ($j = 0; $j < $i; $j++) { + $prevOriginal = $orderAttributes[$j]; + $prevAttr = $this->filter($this->getInternalKeyForAttribute($prevOriginal)); - // Allow after pagination without any order - if (empty($orderAttributes) && !empty($cursor)) { - $orderType = $orderTypes[0] ?? Database::ORDER_ASC; + $bindName = ":cursor_{$j}"; + $binds[$bindName] = $cursor[$prevOriginal]; - if ($cursorDirection === Database::CURSOR_AFTER) { - $orderMethod = $orderType === Database::ORDER_DESC + $conditions[] = "{$this->quote($alias)}.{$this->quote($prevAttr)} = {$bindName}"; + } + + // Add comparison for current attribute + $operator = ($direction === Database::ORDER_DESC) ? Query::TYPE_LESSER : Query::TYPE_GREATER; - } else { - $orderMethod = $orderType === Database::ORDER_DESC - ? Query::TYPE_GREATER - : Query::TYPE_LESSER; - } - $where[] = "({$this->quote($alias)}._id {$this->getSQLOperator($orderMethod)} {$cursor['$sequence']})"; - } + $bindName = ":cursor_{$i}"; + $binds[$bindName] = $cursor[$originalAttribute]; - // Allow order type without any order attribute, fallback to the natural order (_id) - if (!$hasIdAttribute) { - if (empty($orderAttributes) && !empty($orderTypes)) { - $order = $orderTypes[0] ?? Database::ORDER_ASC; - if ($cursorDirection === Database::CURSOR_BEFORE) { - $order = $order === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; - } + $conditions[] = "{$this->quote($alias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}"; - $orders[] = "{$this->quote($alias)}._id ".$this->filter($order); - } else { - $orders[] = "{$this->quote($alias)}._id " . ($cursorDirection === Database::CURSOR_AFTER ? Database::ORDER_ASC : Database::ORDER_DESC); // Enforce last ORDER by '_id' + $cursorWhere[] = '(' . implode(' AND ', $conditions) . ')'; } } + if (!empty($cursorWhere)) { + $where[] = '(' . implode(' OR ', $cursorWhere) . ')'; + } + $conditions = $this->getSQLConditions($queries, $binds); if (!empty($conditions)) { $where[] = $conditions; diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 319f05058..aad33c42e 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -11,7 +11,6 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\NotFound as NotFoundException; -use Utopia\Database\Exception\Order as OrderException; use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Exception\Transaction as TransactionException; use Utopia\Database\Exception\Truncate as TruncateException; @@ -1399,84 +1398,69 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $queries = array_map(fn ($query) => clone $query, $queries); - $hasIdAttribute = false; - foreach ($orderAttributes as $i => $attribute) { - $originalAttribute = $attribute; + $cursorWhere = []; - $attribute = $this->getInternalKeyForAttribute($attribute); + foreach ($orderAttributes as $i => $originalAttribute) { + $attribute = $this->getInternalKeyForAttribute($originalAttribute); $attribute = $this->filter($attribute); - if (\in_array($attribute, ['_uid', '_id'])) { - $hasIdAttribute = true; - } $orderType = $this->filter($orderTypes[$i] ?? Database::ORDER_ASC); + $direction = $orderType; - // Get most dominant/first order attribute - if ($i === 0 && !empty($cursor)) { - $orderMethodSequence = Query::TYPE_GREATER; // To preserve natural order - $orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; + if ($cursorDirection === Database::CURSOR_BEFORE) { + $direction = ($direction === Database::ORDER_ASC) + ? Database::ORDER_DESC + : Database::ORDER_ASC; + } - if ($cursorDirection === Database::CURSOR_BEFORE) { - $orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; - $orderMethodSequence = $orderType === Database::ORDER_ASC ? Query::TYPE_LESSER : Query::TYPE_GREATER; - $orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; - } + $orders[] = "{$this->quote($attribute)} {$direction}"; + + // Build pagination WHERE clause only if we have a cursor + if (!empty($cursor)) { + // Special case: only 1 attribute and it's a unique primary key + if (count($orderAttributes) === 1 && $i === 0 && $originalAttribute === '$sequence') { + $operator = ($direction === Database::ORDER_DESC) + ? Query::TYPE_LESSER + : Query::TYPE_GREATER; + + $bindName = ":cursor_pk"; + $binds[$bindName] = $cursor[$originalAttribute]; - if (\is_null($cursor[$originalAttribute] ?? null)) { - throw new OrderException( - message: "Order attribute '{$originalAttribute}' is empty", - attribute: $originalAttribute - ); + $cursorWhere[] = "{$this->quote($alias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}"; + break; } - $binds[':cursor'] = $cursor[$originalAttribute]; - - $where[] = "( - {$this->quote($alias)}.{$this->quote($attribute)} {$this->getSQLOperator($orderMethod)} :cursor - OR ( - {$this->quote($alias)}.{$this->quote($attribute)} = :cursor - AND - {$this->quote($alias)}._id {$this->getSQLOperator($orderMethodSequence)} {$cursor['$sequence']} - ) - )"; - } elseif ($cursorDirection === Database::CURSOR_BEFORE) { - $orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; - } + $conditions = []; - $orders[] = "{$this->quote($attribute)} {$orderType}"; - } + // Add equality conditions for previous attributes + for ($j = 0; $j < $i; $j++) { + $prevOriginal = $orderAttributes[$j]; + $prevAttr = $this->filter($this->getInternalKeyForAttribute($prevOriginal)); - // Allow after pagination without any order - if (empty($orderAttributes) && !empty($cursor)) { - $orderType = $orderTypes[0] ?? Database::ORDER_ASC; + $bindName = ":cursor_{$j}"; + $binds[$bindName] = $cursor[$prevOriginal]; - if ($cursorDirection === Database::CURSOR_AFTER) { - $orderMethod = $orderType === Database::ORDER_DESC + $conditions[] = "{$this->quote($alias)}.{$this->quote($prevAttr)} = {$bindName}"; + } + + // Add comparison for current attribute + $operator = ($direction === Database::ORDER_DESC) ? Query::TYPE_LESSER : Query::TYPE_GREATER; - } else { - $orderMethod = $orderType === Database::ORDER_DESC - ? Query::TYPE_GREATER - : Query::TYPE_LESSER; - } - $where[] = "({$this->quote($alias)}._id {$this->getSQLOperator($orderMethod)} {$cursor['$sequence']})"; - } + $bindName = ":cursor_{$i}"; + $binds[$bindName] = $cursor[$originalAttribute]; - // Allow order type without any order attribute, fallback to the natural order (_id) - if (!$hasIdAttribute) { - if (empty($orderAttributes) && !empty($orderTypes)) { - $order = $orderTypes[0] ?? Database::ORDER_ASC; - if ($cursorDirection === Database::CURSOR_BEFORE) { - $order = $order === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; - } + $conditions[] = "{$this->quote($alias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}"; - $orders[] = "{$this->quote($alias)}._id ".$this->filter($order); - } else { - $orders[] = "{$this->quote($alias)}._id " . ($cursorDirection === Database::CURSOR_AFTER ? Database::ORDER_ASC : Database::ORDER_DESC); // Enforce last ORDER by '_id' + $cursorWhere[] = '(' . implode(' AND ', $conditions) . ')'; } } + if (!empty($cursorWhere)) { + $where[] = '(' . implode(' OR ', $cursorWhere) . ')'; + } + $conditions = $this->getSQLConditions($queries, $binds); if (!empty($conditions)) { $where[] = $conditions; diff --git a/src/Database/Database.php b/src/Database/Database.php index 6d55e5f17..0658065cc 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -13,6 +13,7 @@ use Utopia\Database\Exception\Index as IndexException; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; +use Utopia\Database\Exception\Order as OrderException; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Exception\Relationship as RelationshipException; use Utopia\Database\Exception\Restricted as RestrictedException; @@ -6014,7 +6015,29 @@ public function find(string $collection, array $queries = [], string $forPermiss $orderAttributes = $grouped['orderAttributes']; $orderTypes = $grouped['orderTypes']; $cursor = $grouped['cursor']; - $cursorDirection = $grouped['cursorDirection']; + $cursorDirection = $grouped['cursorDirection'] ?? Database::CURSOR_AFTER; + + $uniqueOrderBy = false; + foreach ($orderAttributes as $order) { + if ($order === '$id' || $order === '$sequence') { + $uniqueOrderBy = true; + } + } + + if ($uniqueOrderBy === false) { + $orderAttributes[] = '$sequence'; + } + + if (!empty($cursor)) { + foreach ($orderAttributes as $order) { + if ($cursor->getAttribute($order) === null) { + throw new OrderException( + message: "Order attribute '{$order}' is empty", + attribute: $order + ); + } + } + } if (!empty($cursor) && $cursor->getCollection() !== $collection->getId()) { throw new DatabaseException("cursor Document must be from the same Collection."); @@ -6082,7 +6105,7 @@ public function find(string $collection, array $queries = [], string $forPermiss $orderAttributes, $orderTypes, $cursor, - $cursorDirection ?? Database::CURSOR_AFTER, + $cursorDirection, $forPermission ); diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index b4969242a..50bbcba57 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -1900,6 +1900,55 @@ public function testFindOrderByCursorAfter(): void Query::cursorAfter($movies[5]) ]); $this->assertEmpty(count($documents)); + + /** + * Multiple order by, Test tie-break on year 2019 + */ + $movies = $database->find('movies', [ + Query::orderAsc('year'), + Query::orderAsc('price'), + ]); + + $this->assertEquals(6, count($movies)); + + $this->assertEquals($movies[0]['name'], 'Captain America: The First Avenger'); + $this->assertEquals($movies[0]['year'], 2011); + $this->assertEquals($movies[0]['price'], 25.94); + + $this->assertEquals($movies[1]['name'], 'Frozen'); + $this->assertEquals($movies[1]['year'], 2013); + $this->assertEquals($movies[1]['price'], 39.5); + + $this->assertEquals($movies[2]['name'], 'Captain Marvel'); + $this->assertEquals($movies[2]['year'], 2019); + $this->assertEquals($movies[2]['price'], 25.99); + + $this->assertEquals($movies[3]['name'], 'Frozen II'); + $this->assertEquals($movies[3]['year'], 2019); + $this->assertEquals($movies[3]['price'], 39.5); + + $this->assertEquals($movies[4]['name'], 'Work in Progress'); + $this->assertEquals($movies[4]['year'], 2025); + $this->assertEquals($movies[4]['price'], 0); + + $this->assertEquals($movies[5]['name'], 'Work in Progress 2'); + $this->assertEquals($movies[5]['year'], 2026); + $this->assertEquals($movies[5]['price'], 0); + + $pos = 2; + $documents = $database->find('movies', [ + Query::orderAsc('year'), + Query::orderAsc('price'), + Query::cursorAfter($movies[$pos]) + ]); + + $this->assertEquals(3, count($documents)); + + foreach ($documents as $i => $document) { + $this->assertEquals($document['name'], $movies[$i + 1 + $pos]['name']); + $this->assertEquals($document['price'], $movies[$i + 1 + $pos]['price']); + $this->assertEquals($document['year'], $movies[$i + 1 + $pos]['year']); + } }