From 538e22818acce3d468813e6cd3e3342c0792e497 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 22 Jul 2026 16:16:39 +0300 Subject: [PATCH] run tests --- composer.lock | 244 ++++++++++----------- src/Database/Database.php | 54 +++++ tests/e2e/Adapter/Scopes/DocumentTests.php | 211 ++++++++++++++++++ 3 files changed, 381 insertions(+), 128 deletions(-) diff --git a/composer.lock b/composer.lock index d65cb93c8..8eda5b5ee 100644 --- a/composer.lock +++ b/composer.lock @@ -144,23 +144,23 @@ }, { "name": "google/protobuf", - "version": "v4.33.6", + "version": "v5.35.1", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "84b008c23915ed94536737eae46f41ba3bccfe67" + "reference": "55bb4a7d6739b5af0927b96213c1371a3afb7cfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/84b008c23915ed94536737eae46f41ba3bccfe67", - "reference": "84b008c23915ed94536737eae46f41ba3bccfe67", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/55bb4a7d6739b5af0927b96213c1371a3afb7cfb", + "reference": "55bb4a7d6739b5af0927b96213c1371a3afb7cfb", "shasum": "" }, "require": { - "php": ">=8.1.0" + "php": ">=8.2.0" }, "require-dev": { - "phpunit/phpunit": ">=10.5.62 <11.0.0" + "phpunit/phpunit": ">=11.5.0 <12.0.0" }, "suggest": { "ext-bcmath": "Need to support JSON deserialization" @@ -182,9 +182,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.6" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v5.35.1" }, - "time": "2026-03-18T17:32:05+00:00" + "time": "2026-06-11T21:19:23+00:00" }, { "name": "mongodb/mongodb", @@ -409,16 +409,16 @@ }, { "name": "open-telemetry/api", - "version": "1.9.0", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "6f8d237ce2c304ca85f31970f788e7f074d147be" + "reference": "7c029c4a6fd457094a20569bf98f93d95e9a7559" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/6f8d237ce2c304ca85f31970f788e7f074d147be", - "reference": "6f8d237ce2c304ca85f31970f788e7f074d147be", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/7c029c4a6fd457094a20569bf98f93d95e9a7559", + "reference": "7c029c4a6fd457094a20569bf98f93d95e9a7559", "shasum": "" }, "require": { @@ -475,7 +475,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2026-02-25T13:24:05+00:00" + "time": "2026-07-06T12:28:04+00:00" }, { "name": "open-telemetry/context", @@ -602,20 +602,20 @@ }, { "name": "open-telemetry/gen-otlp-protobuf", - "version": "1.9.0", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git", - "reference": "a229cf161d42001d64c8f21e8f678581fe1c66b9" + "reference": "66f04d0e448ad333033bfc7baae1aa56330be088" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/a229cf161d42001d64c8f21e8f678581fe1c66b9", - "reference": "a229cf161d42001d64c8f21e8f678581fe1c66b9", + "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/66f04d0e448ad333033bfc7baae1aa56330be088", + "reference": "66f04d0e448ad333033bfc7baae1aa56330be088", "shasum": "" }, "require": { - "google/protobuf": "^3.22 || ^4.0", + "google/protobuf": "^3.22 || ^4.0 || ^5.0", "php": "^8.0" }, "suggest": { @@ -661,20 +661,20 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-10-19T06:44:33+00:00" + "time": "2026-06-17T12:06:32+00:00" }, { "name": "open-telemetry/sdk", - "version": "1.14.0", + "version": "1.15.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "6e3d0ce93e76555dd5e2f1d19443ff45b990e410" + "reference": "77e1aa73850154abb86937d52a70883edc3b4547" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/6e3d0ce93e76555dd5e2f1d19443ff45b990e410", - "reference": "6e3d0ce93e76555dd5e2f1d19443ff45b990e410", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/77e1aa73850154abb86937d52a70883edc3b4547", + "reference": "77e1aa73850154abb86937d52a70883edc3b4547", "shasum": "" }, "require": { @@ -682,7 +682,7 @@ "nyholm/psr7-server": "^1.1", "open-telemetry/api": "^1.8", "open-telemetry/context": "^1.4", - "open-telemetry/sem-conv": "^1.0", + "open-telemetry/sem-conv": "^1.38.0", "php": "^8.1", "php-http/discovery": "^1.14", "psr/http-client": "^1.0", @@ -705,7 +705,10 @@ "spi": { "OpenTelemetry\\API\\Configuration\\ConfigEnv\\EnvComponentLoader": [ "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderHttpConfig", - "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderPeerConfig" + "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderPeerConfig", + "OpenTelemetry\\SDK\\ConfigEnv\\Trace\\SpanSuppressionStrategySemConv", + "OpenTelemetry\\SDK\\ConfigEnv\\Trace\\SpanSuppressionStrategySpanKind", + "OpenTelemetry\\SDK\\ConfigEnv\\Distribution\\DistributionConfigurationSdk" ], "OpenTelemetry\\SDK\\Common\\Configuration\\Resolver\\ResolverInterface": [ "OpenTelemetry\\SDK\\Common\\Configuration\\Resolver\\SdkConfigurationResolver" @@ -715,7 +718,7 @@ ] }, "branch-alias": { - "dev-main": "1.12.x-dev" + "dev-main": "1.14.x-dev" } }, "autoload": { @@ -758,7 +761,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2026-03-21T11:50:01+00:00" + "time": "2026-07-14T13:09:54+00:00" }, { "name": "open-telemetry/sem-conv", @@ -1315,16 +1318,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -1362,7 +1365,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.7.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -1382,20 +1385,20 @@ "type": "tidelift" } ], - "time": "2026-04-13T15:52:40+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/http-client", - "version": "v7.4.13", + "version": "v7.4.14", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "e8a112b8415707265a7e614278136a9d92989a6a" + "reference": "f6bc6b5a54ff5afac4725cacec9bf2f52eb15920" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/e8a112b8415707265a7e614278136a9d92989a6a", - "reference": "e8a112b8415707265a7e614278136a9d92989a6a", + "url": "https://api.github.com/repos/symfony/http-client/zipball/f6bc6b5a54ff5afac4725cacec9bf2f52eb15920", + "reference": "f6bc6b5a54ff5afac4725cacec9bf2f52eb15920", "shasum": "" }, "require": { @@ -1463,7 +1466,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.13" + "source": "https://github.com/symfony/http-client/tree/v7.4.14" }, "funding": [ { @@ -1483,20 +1486,20 @@ "type": "tidelift" } ], - "time": "2026-05-24T09:57:54+00:00" + "time": "2026-06-16T11:50:14+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d" + "reference": "41fc42d276aeff21192465331ebbab7d83a743c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d", - "reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41fc42d276aeff21192465331ebbab7d83a743c0", + "reference": "41fc42d276aeff21192465331ebbab7d83a743c0", "shasum": "" }, "require": { @@ -1545,7 +1548,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.1" }, "funding": [ { @@ -1565,7 +1568,7 @@ "type": "tidelift" } ], - "time": "2026-03-06T13:17:50+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -1814,16 +1817,16 @@ }, { "name": "symfony/polyfill-php85", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee" + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee", - "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", "shasum": "" }, "require": { @@ -1870,7 +1873,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" }, "funding": [ { @@ -1890,20 +1893,20 @@ "type": "tidelift" } ], - "time": "2026-04-26T13:10:57+00:00" + "time": "2026-05-26T02:25:22+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", "shasum": "" }, "require": { @@ -1957,7 +1960,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" }, "funding": [ { @@ -1977,7 +1980,7 @@ "type": "tidelift" } ], - "time": "2026-03-28T09:44:51+00:00" + "time": "2026-06-16T09:55:08+00:00" }, { "name": "tbachert/spi", @@ -2033,16 +2036,16 @@ }, { "name": "utopia-php/cache", - "version": "3.2.0", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "9511d854dcb39e5bab7e08a83791cd30cc68627e" + "reference": "5c292d4df156f8b008f29fa6b033834b906bf328" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/9511d854dcb39e5bab7e08a83791cd30cc68627e", - "reference": "9511d854dcb39e5bab7e08a83791cd30cc68627e", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/5c292d4df156f8b008f29fa6b033834b906bf328", + "reference": "5c292d4df156f8b008f29fa6b033834b906bf328", "shasum": "" }, "require": { @@ -2081,9 +2084,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/3.2.0" + "source": "https://github.com/utopia-php/cache/tree/3.4.0" }, - "time": "2026-07-01T08:45:24+00:00" + "time": "2026-07-01T15:44:35+00:00" }, { "name": "utopia-php/circuit-breaker", @@ -2197,16 +2200,16 @@ }, { "name": "utopia-php/mongo", - "version": "1.1.0", + "version": "1.3.2", "source": { "type": "git", "url": "https://github.com/utopia-php/mongo.git", - "reference": "73593682deee4696525a04e26524c1c1226e1530" + "reference": "34d9ab406a87d2006b0e108796fdb6b5397853d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/mongo/zipball/73593682deee4696525a04e26524c1c1226e1530", - "reference": "73593682deee4696525a04e26524c1c1226e1530", + "url": "https://api.github.com/repos/utopia-php/mongo/zipball/34d9ab406a87d2006b0e108796fdb6b5397853d3", + "reference": "34d9ab406a87d2006b0e108796fdb6b5397853d3", "shasum": "" }, "require": { @@ -2252,22 +2255,22 @@ ], "support": { "issues": "https://github.com/utopia-php/mongo/issues", - "source": "https://github.com/utopia-php/mongo/tree/1.1.0" + "source": "https://github.com/utopia-php/mongo/tree/1.3.2" }, - "time": "2026-04-24T06:15:10+00:00" + "time": "2026-07-20T09:02:13+00:00" }, { "name": "utopia-php/pools", - "version": "1.0.8", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/utopia-php/pools.git", - "reference": "b685ca01883ed820b9898b85163a8f3d970a2da7" + "reference": "fd5aa7e89685ffc74f9dff3d23c270ef30c7bbf3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/pools/zipball/b685ca01883ed820b9898b85163a8f3d970a2da7", - "reference": "b685ca01883ed820b9898b85163a8f3d970a2da7", + "url": "https://api.github.com/repos/utopia-php/pools/zipball/fd5aa7e89685ffc74f9dff3d23c270ef30c7bbf3", + "reference": "fd5aa7e89685ffc74f9dff3d23c270ef30c7bbf3", "shasum": "" }, "require": { @@ -2278,7 +2281,10 @@ "swoole/ide-helper": "6.*" }, "suggest": { - "ext-swoole": "Required to use the Swoole pool adapter" + "ext-mongodb": "Needed to support MongoDB database pools", + "ext-pdo": "Needed to support MariaDB, MySQL or SQLite database pools", + "ext-redis": "Needed to support Redis cache pools", + "ext-swoole": "Needed to support Swoole based pool adapter" }, "type": "library", "autoload": { @@ -2305,26 +2311,25 @@ ], "support": { "issues": "https://github.com/utopia-php/pools/issues", - "source": "https://github.com/utopia-php/pools/tree/1.0.8" + "source": "https://github.com/utopia-php/pools/tree/1.1.0" }, - "time": "2026-06-20T09:45:06+00:00" + "time": "2026-07-17T10:19:09+00:00" }, { "name": "utopia-php/telemetry", - "version": "0.4.3", + "version": "0.4.5", "source": { "type": "git", "url": "https://github.com/utopia-php/telemetry.git", - "reference": "50491fb1686a9796e9cd5005ede6141e2ceeb5ac" + "reference": "139943bffcd4f6dd8fb9ed247f946a1d151b006a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/telemetry/zipball/50491fb1686a9796e9cd5005ede6141e2ceeb5ac", - "reference": "50491fb1686a9796e9cd5005ede6141e2ceeb5ac", + "url": "https://api.github.com/repos/utopia-php/telemetry/zipball/139943bffcd4f6dd8fb9ed247f946a1d151b006a", + "reference": "139943bffcd4f6dd8fb9ed247f946a1d151b006a", "shasum": "" }, "require": { - "ext-opentelemetry": "*", "ext-protobuf": "*", "nyholm/psr7": "1.*", "open-telemetry/exporter-otlp": "1.*", @@ -2333,7 +2338,6 @@ "symfony/http-client": "7.*" }, "require-dev": { - "phpbench/phpbench": "1.*", "swoole/ide-helper": "6.*" }, "suggest": { @@ -2358,22 +2362,22 @@ ], "support": { "issues": "https://github.com/utopia-php/telemetry/issues", - "source": "https://github.com/utopia-php/telemetry/tree/0.4.3" + "source": "https://github.com/utopia-php/telemetry/tree/0.4.5" }, - "time": "2026-06-20T09:45:06+00:00" + "time": "2026-07-08T11:07:25+00:00" }, { "name": "utopia-php/validators", - "version": "0.3.0", + "version": "0.3.1", "source": { "type": "git", "url": "https://github.com/utopia-php/validators.git", - "reference": "5a3ef67ce17c2e148da7895e8de699614f0afdfa" + "reference": "d9c2269ebd2596a09681ccd2fd133eeedfcdf9ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/validators/zipball/5a3ef67ce17c2e148da7895e8de699614f0afdfa", - "reference": "5a3ef67ce17c2e148da7895e8de699614f0afdfa", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/d9c2269ebd2596a09681ccd2fd133eeedfcdf9ba", + "reference": "d9c2269ebd2596a09681ccd2fd133eeedfcdf9ba", "shasum": "" }, "require": { @@ -2398,9 +2402,9 @@ ], "support": { "issues": "https://github.com/utopia-php/validators/issues", - "source": "https://github.com/utopia-php/validators/tree/0.3.0" + "source": "https://github.com/utopia-php/validators/tree/0.3.1" }, - "time": "2026-07-13T09:46:33+00:00" + "time": "2026-07-14T11:55:48+00:00" } ], "packages-dev": [ @@ -2538,16 +2542,16 @@ }, { "name": "laravel/pint", - "version": "v1.29.1", + "version": "v1.29.3", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80" + "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/0770e9b7fafd50d4586881d456d6eb41c9247a80", - "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80", + "url": "https://api.github.com/repos/laravel/pint/zipball/da1d1111a6aa2e082d2a388b194afe1ba0a05d14", + "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14", "shasum": "" }, "require": { @@ -2558,14 +2562,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.95.1", - "illuminate/view": "^12.56.0", - "larastan/larastan": "^3.9.6", + "friendsofphp/php-cs-fixer": "^3.95.8", + "illuminate/view": "^12.62.0", + "larastan/larastan": "^3.10.0", "laravel-zero/framework": "^12.1.0", + "laravel/agent-detector": "^2.0.2", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.4.0", - "pestphp/pest": "^3.8.6", - "shipfastlabs/agent-detector": "^1.1.3" + "pestphp/pest": "^3.8.6" }, "bin": [ "builds/pint" @@ -2602,7 +2606,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-04-20T15:26:14+00:00" + "time": "2026-06-16T15:34:04+00:00" }, { "name": "myclabs/deep-copy", @@ -3241,25 +3245,25 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.34", + "version": "9.6.35", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b36f02317466907a230d3aa1d34467041271ef4a" + "reference": "0edba2f3a0c48df3553cb9b640810b30df60302b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", - "reference": "b36f02317466907a230d3aa1d34467041271ef4a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0edba2f3a0c48df3553cb9b640810b30df60302b", + "reference": "0edba2f3a0c48df3553cb9b640810b30df60302b", "shasum": "" }, "require": { "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", + "ext-filter": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", - "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", @@ -3324,31 +3328,15 @@ "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.34" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.35" }, "funding": [ { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "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/phpunit/phpunit", - "type": "tidelift" + "url": "https://phpunit.de/sponsoring.html", + "type": "other" } ], - "time": "2026-01-27T05:45:00+00:00" + "time": "2026-07-06T14:48:07+00:00" }, { "name": "rregeer/phpunit-coverage-check", @@ -4645,7 +4633,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -4655,6 +4643,6 @@ "ext-mbstring": "*", "ext-redis": "*" }, - "platform-dev": {}, - "plugin-api-version": "2.9.0" + "platform-dev": [], + "plugin-api-version": "2.6.0" } diff --git a/src/Database/Database.php b/src/Database/Database.php index cc87483a8..4af62cc6c 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -179,6 +179,12 @@ class Database // Cache public const TTL = 60 * 60 * 24; // 24 hours + // Marker key stored in place of a document to represent a cached "not + // found" result. Attribute keys cannot start with '$', so this never + // collides with a real document payload. Lives under the standard TTL and + // is purged by the create/upsert paths so a newly inserted id is visible. + private const CACHE_EMPTY_MARKER = '$empty'; + // Events public const EVENT_ALL = '*'; @@ -4875,6 +4881,11 @@ public function getDocument(string $collection, string $id, array $queries = [], } } + // Negative cache hit + if (\is_array($cached) && isset($cached[self::CACHE_EMPTY_MARKER])) { + return $this->createDocumentInstance($collection->getId(), []); + } + if ($cached) { $document = $this->createDocumentInstance($collection->getId(), $cached); @@ -4921,6 +4932,18 @@ public function getDocument(string $collection, string $id, array $queries = [], ); if ($document->isEmpty()) { + if (!$forUpdate && empty($relationships)) { + try { + $marker = [self::CACHE_EMPTY_MARKER => true]; + + if ($this->cache->saveWithLease($documentKey, $marker, $hashKey, $generation) !== false) { + $this->cache->save($collectionKey, 'empty', $documentKey); + } + } catch (Exception $e) { + Console::warning('Failed to save empty document to cache: ' . $e->getMessage()); + } + } + return $this->createDocumentInstance($collection->getId(), []); } @@ -5700,6 +5723,10 @@ public function createDocument(string $collection, Document $document): Document return $this->adapter->createDocument($collection, $document); }); + // Clear any negative-cache entry for this id: a prior read may have + // recorded it as missing before this insert committed. + $this->purgeCreatedDocumentCache($collection, $document); + if (!$this->inBatchRelationshipPopulation && $this->resolveRelationships) { // Use the write stack depth for proper MAX_DEPTH enforcement during creation $fetchDepth = count($this->relationshipWriteStack); @@ -5826,6 +5853,9 @@ public function createDocuments( $document = $this->casting($collection, $document); $document = $this->decode($collection, $document); + // Clear any negative-cache entry recorded before this insert. + $this->purgeCreatedDocumentCache($collection, $document); + try { $onNext && $onNext($document); } catch (\Throwable $e) { @@ -8427,6 +8457,30 @@ protected function purgeCachedDocumentInternal(string $collectionId, ?string $id return true; } + /** + * Invalidate any cache entry for a freshly created document. + * + * A read of a not-yet-created id caches a negative ("not found") marker; + * once the id is inserted that marker must be cleared so the document + * becomes visible immediately. Resolves the correct tenant first when + * tenant-per-document is enabled, so the purge targets the right key. + * + * @param Document $collection + * @param Document $document + * @return void + * @throws Exception + */ + private function purgeCreatedDocumentCache(Document $collection, Document $document): void + { + if ($this->getSharedTables() && $this->getTenantPerDocument()) { + $this->withTenant($document->getTenant(), function () use ($collection, $document) { + $this->purgeCachedDocumentInternal($collection->getId(), $document->getId()); + }); + } else { + $this->purgeCachedDocumentInternal($collection->getId(), $document->getId()); + } + } + /** * Cleans a specific document from cache and triggers EVENT_DOCUMENT_PURGE. * And related document reference in the collection cache. diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 6f16a5a81..2cd624fe1 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -664,6 +664,217 @@ public function testCreateDocuments(): void } } + public function testCacheEmptyDocument(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + $collection = 'cacheEmpty'; + $database->createCollection($collection, permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + ], documentSecurity: false); + $this->assertEquals(true, $database->createAttribute($collection, 'name', Database::VAR_STRING, 128, false)); + + $cache = $database->getCache(); + + // A read of a missing id records a negative ("not found") marker so + // repeated lookups don't keep hitting the adapter. + $this->assertTrue($database->getDocument($collection, 'ghost')->isEmpty()); + + [, $documentKey, $hashKey] = $database->getCacheKeys($collection, 'ghost'); + $cached = $cache->load($documentKey, Database::TTL, $hashKey); + $this->assertIsArray($cached); + $this->assertArrayHasKey('$empty', $cached); // Database::CACHE_EMPTY_MARKER + + // Creating the id must invalidate that marker so the row is visible. + $database->createDocument($collection, new Document([ + '$id' => 'ghost', + '$permissions' => [Permission::read(Role::any())], + 'name' => 'real', + ])); + + $this->assertFalse($cache->load($documentKey, Database::TTL, $hashKey)); + + $document = $database->getDocument($collection, 'ghost'); + $this->assertFalse($document->isEmpty()); + $this->assertEquals('real', $document->getAttribute('name')); + + // Same guarantee through the batch create path. + $this->assertTrue($database->getDocument($collection, 'batch')->isEmpty()); + [, $batchKey, $batchHash] = $database->getCacheKeys($collection, 'batch'); + $this->assertArrayHasKey('$empty', $cache->load($batchKey, Database::TTL, $batchHash)); + + $database->createDocuments($collection, [ + new Document([ + '$id' => 'batch', + '$permissions' => [Permission::read(Role::any())], + 'name' => 'batched', + ]), + ]); + + $this->assertFalse($cache->load($batchKey, Database::TTL, $batchHash)); + $this->assertEquals('batched', $database->getDocument($collection, 'batch')->getAttribute('name')); + + // A locking read must never publish anything to the cache. + $this->assertTrue($database->getDocument($collection, 'phantom', forUpdate: true)->isEmpty()); + [, $phantomKey, $phantomHash] = $database->getCacheKeys($collection, 'phantom'); + $this->assertFalse($cache->load($phantomKey, Database::TTL, $phantomHash)); + } + + public function testCacheEmptyDocumentSelect(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + $cache = $database->getCache(); + + $collection = 'cacheEmptySelect'; + $database->createCollection($collection, permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + ], documentSecurity: false); + + $this->assertEquals(true, $database->createAttribute($collection, 'name', Database::VAR_STRING, 128, false)); + + // The document key is select-independent, but the hashKey is not: a + // projection is folded into it. So a projected read and a plain read of + // the same missing id are cached under different slots of the same key. + [, $documentKey, $plainHash] = $database->getCacheKeys($collection, 'ghost'); + + // validateSelections() appends the internal attributes to the user + // selection before it forms the key; mirror that set to address the + // projected slot (getCacheKeys sorts, so order does not matter). + $selects = ['name', '$id', '$sequence', '$collection', '$createdAt', '$updatedAt', '$permissions']; + [, , $selectHash] = $database->getCacheKeys($collection, 'ghost', $selects); + $this->assertNotEquals($plainHash, $selectHash); + + // Projected read caches its marker under the projected slot only. + $this->assertTrue($database->getDocument($collection, 'ghost', [Query::select(['name'])])->isEmpty()); + $this->assertArrayHasKey('$empty', $cache->load($documentKey, Database::TTL, $selectHash)); + $this->assertFalse( + $cache->load($documentKey, Database::TTL, $plainHash), + 'A projected read must not populate the no-projection cache slot' + ); + + // Plain read fills the plain slot with its own marker. Both slots of the + // document key now hold an "empty" marker. + $this->assertTrue($database->getDocument($collection, 'ghost')->isEmpty()); + $this->assertArrayHasKey('$empty', $cache->load($documentKey, Database::TTL, $plainHash)); + + // Inserting the id purges the whole document key, so BOTH slots clear. + $database->createDocument($collection, new Document([ + '$id' => 'ghost', + '$permissions' => [Permission::read(Role::any())], + 'name' => 'real', + ])); + + $this->assertFalse($cache->load($documentKey, Database::TTL, $plainHash)); + $this->assertFalse($cache->load($documentKey, Database::TTL, $selectHash)); + } + + public function testCacheEmptyGetCollection(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + $cache = $database->getCache(); + + $collectionId = 'cacheEmptyCollection'; + + // getCollection() reads getDocument(METADATA, id) under the hood, so a + // lookup of a non-existent collection negatively caches its absence + // under the metadata key. + $this->assertTrue($database->getCollection($collectionId)->isEmpty()); + + [, $metaKey, $metaHash] = $database->getCacheKeys(Database::METADATA, $collectionId); + $cached = $cache->load($metaKey, Database::TTL, $metaHash); + $this->assertIsArray($cached); + $this->assertArrayHasKey('$empty', $cached); + + // createCollection() writes the metadata row via createDocument(METADATA), + // which must purge that marker — otherwise the collection would keep + // reading back as "not found". + $collection = $database->createCollection($collectionId, permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + ], documentSecurity: false); + $this->assertFalse($collection->isEmpty()); + + $this->assertFalse($cache->load($metaKey, Database::TTL, $metaHash)); + + $fetched = $database->getCollection($collectionId); + $this->assertFalse($fetched->isEmpty()); + $this->assertEquals($collectionId, $fetched->getId()); + + // Recreating it must now be rejected as a duplicate. This proves the + // marker was genuinely invalidated: a lingering "not found" would make + // createCollection's own existence check pass and wrongly proceed. + try { + $database->createCollection($collectionId); + $this->fail('Expected DuplicateException when recreating an existing collection'); + } catch (DuplicateException) { + // expected + } + } + + public function testCacheEmptyDocumentSecurity(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + $auth = $database->getAuthorization(); + $cache = $database->getCache(); + + $collection = 'cacheEmptyDocSecurity'; + + // Document-level security with no collection-wide read: access is + // decided per document. + $auth->skip(function () use ($database, $collection) { + $database->createCollection($collection, permissions: [], documentSecurity: true); + $this->assertEquals(true, $database->createAttribute($collection, 'name', Database::VAR_STRING, 128, false)); + $database->createDocument($collection, new Document([ + '$id' => 'secret', + '$permissions' => [ + Permission::read(Role::user('userA')), + ], + 'name' => 'classified', + ])); + }); + + try { + // userB cannot read 'secret'. The row exists, so this denial must + // NOT record a negative marker under the shared (user-independent) + // cache key — doing so would hide the row from userA. + $auth->cleanRoles(); + $auth->addRole(Role::user('userB')->toString()); + + $this->assertTrue($database->getDocument($collection, 'secret')->isEmpty()); + + [, $documentKey, $hashKey] = $database->getCacheKeys($collection, 'secret'); + $cached = $cache->load($documentKey, Database::TTL, $hashKey); + $this->assertFalse( + \is_array($cached) && isset($cached['$empty']), + 'A permission-denied read of an existing document must not populate the negative cache' + ); + + // userA has read permission and must still see the document, + // proving userB's forbidden read did not poison the cache. + $auth->cleanRoles(); + $auth->addRole(Role::user('userA')->toString()); + + $document = $database->getDocument($collection, 'secret'); + $this->assertFalse($document->isEmpty()); + $this->assertEquals('classified', $document->getAttribute('name')); + + // A genuinely missing id is user-independent, so it is still safe to + // cache as empty even under document security. + $this->assertTrue($database->getDocument($collection, 'ghost')->isEmpty()); + [, $ghostKey, $ghostHash] = $database->getCacheKeys($collection, 'ghost'); + $this->assertArrayHasKey('$empty', $cache->load($ghostKey, Database::TTL, $ghostHash)); + } finally { + $auth->cleanRoles(); + $auth->addRole(Role::any()->toString()); + } + } + public function testCreateDocumentsWithAutoIncrement(): void { /** @var Database $database */