From a259e294c5c39ac80c68ef4e090c5ad80e73d2c7 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 14 Aug 2026 16:34:48 +1200 Subject: [PATCH 01/14] feat: adapt migration schema calls to query-lib value objects Published 2.0 still used Database::VAR_* and positional createAttribute/createIndex, which feat-query-lib removed. Rebase onto main and pass Attribute/Index/Relationship VOs plus ColumnType/IndexType so Appwrite can pin this branch as 2.0.0. --- bin/MigrationCLI.php | 36 +- composer.json | 15 +- composer.lock | 475 ++++++++++++++---- src/Migration/Destinations/Appwrite.php | 276 +++++----- .../Database/Columns/Relationship.php | 7 +- src/Migration/Sources/Appwrite.php | 6 +- .../Sources/Appwrite/Reader/Database.php | 15 +- src/Migration/Sources/CSV.php | 8 +- .../AppwriteDatabaseStatusTest.php | 37 +- .../Destinations/AppwriteIndexLengthsTest.php | 104 ++-- 10 files changed, 673 insertions(+), 306 deletions(-) diff --git a/bin/MigrationCLI.php b/bin/MigrationCLI.php index 96cd99dd..e6681148 100644 --- a/bin/MigrationCLI.php +++ b/bin/MigrationCLI.php @@ -19,6 +19,8 @@ use Utopia\Migration\Sources\NHost; use Utopia\Migration\Sources\Supabase; use Utopia\Migration\Transfer; +use Utopia\Query\Schema\ColumnType; +use Utopia\Query\Schema\IndexType; /** * Migrations CLI Tool @@ -38,7 +40,7 @@ class MigrationCLI 'attributes' => [ [ '$id' => 'databaseInternalId', - 'type' => Database::VAR_STRING, + 'type' => ColumnType::String->value, 'format' => '', 'size' => Database::LENGTH_KEY, 'signed' => true, @@ -49,7 +51,7 @@ class MigrationCLI ], [ '$id' => 'databaseId', - 'type' => Database::VAR_STRING, + 'type' => ColumnType::String->value, 'signed' => true, 'size' => Database::LENGTH_KEY, 'format' => '', @@ -60,7 +62,7 @@ class MigrationCLI ], [ '$id' => 'name', - 'type' => Database::VAR_STRING, + 'type' => ColumnType::String->value, 'size' => Database::LENGTH_KEY, 'required' => true, 'signed' => true, @@ -69,7 +71,7 @@ class MigrationCLI ], [ '$id' => 'enabled', - 'type' => Database::VAR_BOOLEAN, + 'type' => ColumnType::Boolean->value, 'signed' => true, 'size' => 0, 'format' => '', @@ -80,7 +82,7 @@ class MigrationCLI ], [ '$id' => 'documentSecurity', - 'type' => Database::VAR_BOOLEAN, + 'type' => ColumnType::Boolean->value, 'signed' => true, 'size' => 0, 'format' => '', @@ -91,7 +93,7 @@ class MigrationCLI ], [ '$id' => 'attributes', - 'type' => Database::VAR_STRING, + 'type' => ColumnType::String->value, 'size' => 1000000, 'required' => false, 'signed' => true, @@ -100,7 +102,7 @@ class MigrationCLI ], [ '$id' => 'indexes', - 'type' => Database::VAR_STRING, + 'type' => ColumnType::String->value, 'size' => 1000000, 'required' => false, 'signed' => true, @@ -109,7 +111,7 @@ class MigrationCLI ], [ '$id' => 'search', - 'type' => Database::VAR_STRING, + 'type' => ColumnType::String->value, 'format' => '', 'size' => 16384, 'signed' => true, @@ -122,31 +124,31 @@ class MigrationCLI 'indexes' => [ [ '$id' => '_fulltext_search', - 'type' => Database::INDEX_FULLTEXT, + 'type' => IndexType::Fulltext->value, 'attributes' => ['search'], 'lengths' => [], 'orders' => [], ], [ '$id' => '_key_name', - 'type' => Database::INDEX_KEY, + 'type' => IndexType::Key->value, 'attributes' => ['name'], 'lengths' => [Database::LENGTH_KEY], - 'orders' => [Database::ORDER_ASC], + 'orders' => ['ASC'], ], [ '$id' => '_key_enabled', - 'type' => Database::INDEX_KEY, + 'type' => IndexType::Key->value, 'attributes' => ['enabled'], 'lengths' => [], - 'orders' => [Database::ORDER_ASC], + 'orders' => ['ASC'], ], [ '$id' => '_key_documentSecurity', - 'type' => Database::INDEX_KEY, + 'type' => IndexType::Key->value, 'attributes' => ['documentSecurity'], 'lengths' => [], - 'orders' => [Database::ORDER_ASC], + 'orders' => ['ASC'], ], ], ]; @@ -280,7 +282,7 @@ function (mixed $value, Document $document, Database $database) { $attributeType = $attribute->getAttribute('type'); switch ($attributeType) { - case Database::VAR_RELATIONSHIP: + case ColumnType::Relationship->value: $options = $attribute->getAttribute('options'); foreach ($options as $key => $value) { $attribute->setAttribute($key, $value); @@ -288,7 +290,7 @@ function (mixed $value, Document $document, Database $database) { $attribute->removeAttribute('options'); break; - case Database::VAR_STRING: + case ColumnType::String->value: $filters = $attribute->getAttribute('filters', []); $attribute->setAttribute('encrypt', in_array('encrypt', $filters)); break; diff --git a/composer.json b/composer.json index 1f8625c4..78ad8ffb 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "migration" ], "license": "MIT", - "minimum-stability": "stable", + "minimum-stability": "dev", + "prefer-stable": true, "autoload": { "psr-4": { "Utopia\\Migration\\": "src/Migration" @@ -32,7 +33,7 @@ "ext-curl": "*", "ext-openssl": "*", "appwrite/appwrite": "^27.0", - "utopia-php/database": "^7.0.0", + "utopia-php/database": "dev-feat-query-lib as 7.0.0", "utopia-php/storage": "4.*", "utopia-php/dsn": "0.2.*", "halaxa/json-machine": "^1.2" @@ -44,6 +45,16 @@ "laravel/pint": "1.*", "phpstan/phpstan": "1.*" }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/utopia-php/database.git" + }, + { + "type": "vcs", + "url": "https://github.com/utopia-php/async.git" + } + ], "config": { "allow-plugins": { "php-http/discovery": true, diff --git a/composer.lock b/composer.lock index ee1336d9..1ca9e57b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fea66e8c192e5b3d1dc56c953be0c2eb", + "content-hash": "e20f678bfb397f89a6cfa0083988f539", "packages": [ { "name": "adhocore/jwt", @@ -986,6 +986,71 @@ }, "time": "2026-01-21T04:14:03+00:00" }, + { + "name": "opis/closure", + "version": "4.5.0", + "source": { + "type": "git", + "url": "https://github.com/opis/closure.git", + "reference": "b97e42b95bb72d87507f5e2d137ceb239aea8d6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/closure/zipball/b97e42b95bb72d87507f5e2d137ceb239aea8d6b", + "reference": "b97e42b95bb72d87507f5e2d137ceb239aea8d6b", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Opis\\Closure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary data.", + "homepage": "https://opis.io/closure", + "keywords": [ + "anonymous classes", + "anonymous functions", + "closure", + "function", + "serializable", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/opis/closure/issues", + "source": "https://github.com/opis/closure/tree/4.5.0" + }, + "time": "2026-03-05T13:32:42+00:00" + }, { "name": "php-http/discovery", "version": "1.20.0", @@ -1555,16 +1620,16 @@ }, { "name": "symfony/http-client", - "version": "v7.4.14", + "version": "v7.4.16", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "f6bc6b5a54ff5afac4725cacec9bf2f52eb15920" + "reference": "c513ed0ba5d1784a6b55fc84190dbe4451b12f41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/f6bc6b5a54ff5afac4725cacec9bf2f52eb15920", - "reference": "f6bc6b5a54ff5afac4725cacec9bf2f52eb15920", + "url": "https://api.github.com/repos/symfony/http-client/zipball/c513ed0ba5d1784a6b55fc84190dbe4451b12f41", + "reference": "c513ed0ba5d1784a6b55fc84190dbe4451b12f41", "shasum": "" }, "require": { @@ -1632,7 +1697,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.14" + "source": "https://github.com/symfony/http-client/tree/v7.4.16" }, "funding": [ { @@ -1652,7 +1717,7 @@ "type": "tidelift" } ], - "time": "2026-06-16T11:50:14+00:00" + "time": "2026-07-29T16:20:51+00:00" }, { "name": "symfony/http-client-contracts", @@ -1903,16 +1968,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.38.2", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8" + "reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8", - "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/5ea99087fb99c273a9b9236ed4c31e78b16103c6", + "reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6", "shasum": "" }, "require": { @@ -1959,7 +2024,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.41.0" }, "funding": [ { @@ -1979,20 +2044,20 @@ "type": "tidelift" } ], - "time": "2026-05-27T06:51:48+00:00" + "time": "2026-07-01T12:47:55+00:00" }, { "name": "symfony/polyfill-php85", - "version": "v1.38.1", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", "shasum": "" }, "require": { @@ -2039,7 +2104,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" }, "funding": [ { @@ -2059,7 +2124,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T02:25:22+00:00" + "time": "2026-07-01T12:47:55+00:00" }, { "name": "symfony/service-contracts", @@ -2200,35 +2265,152 @@ }, "time": "2025-06-29T15:42:06+00:00" }, + { + "name": "utopia-php/async", + "version": "0.1.1", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/async.git", + "reference": "3ee4fc3d505113d0d6050f35f5bf85e706866a22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/async/zipball/3ee4fc3d505113d0d6050f35f5bf85e706866a22", + "reference": "3ee4fc3d505113d0d6050f35f5bf85e706866a22", + "shasum": "" + }, + "require": { + "opis/closure": "4.*", + "php": ">=8.1" + }, + "require-dev": { + "amphp/amp": "3.*", + "amphp/parallel": "2.*", + "amphp/process": "^2.0", + "laravel/pint": "1.*", + "phpstan/phpstan": "2.*", + "phpunit/phpunit": "11.5.45", + "react/child-process": "0.*", + "react/event-loop": "1.*", + "swoole/ide-helper": "*" + }, + "suggest": { + "amphp/amp": "Required for Amp promise adapter", + "amphp/parallel": "Required for Amp parallel adapter", + "ext-ev": "Required for ReactPHP event loop (recommended for best performance)", + "ext-parallel": "Required for parallel adapter (requires PHP ZTS build)", + "ext-sockets": "Required for Swoole Process adapter", + "ext-swoole": "Required for Swoole Thread and Process adapters (recommended for best performance)", + "react/child-process": "Required for ReactPHP parallel adapter", + "react/event-loop": "Required for ReactPHP promise and parallel adapters" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Async\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Utopia\\Tests\\": "tests/" + } + }, + "scripts": { + "test-unit": [ + "vendor/bin/phpunit tests/Unit --exclude-group no-swoole" + ], + "test-promise-sync": [ + "vendor/bin/phpunit tests/E2e/Promise/SyncTest.php" + ], + "test-promise-swoole": [ + "vendor/bin/phpunit tests/E2e/Promise/Swoole" + ], + "test-promise-amp": [ + "vendor/bin/phpunit tests/E2e/Promise/Amp" + ], + "test-promise-react": [ + "vendor/bin/phpunit tests/E2e/Promise/React" + ], + "test-parallel-sync": [ + "vendor/bin/phpunit tests/E2e/Parallel/Sync" + ], + "test-parallel-swoole-thread": [ + "vendor/bin/phpunit tests/E2e/Parallel/Swoole/ThreadTest.php" + ], + "test-parallel-swoole-process": [ + "vendor/bin/phpunit tests/E2e/Parallel/Swoole/ProcessTest.php" + ], + "test-parallel-amp": [ + "vendor/bin/phpunit tests/E2e/Parallel/Amp" + ], + "test-parallel-react": [ + "vendor/bin/phpunit tests/E2e/Parallel/React" + ], + "test-parallel-ext": [ + "php -n -d extension=parallel.so -d extension=sockets.so vendor/bin/phpunit tests/E2e/Parallel/Parallel" + ], + "test-e2e": [ + "vendor/bin/phpunit tests/E2e --exclude-group ext-parallel" + ], + "test": [ + "@test-unit", + "@test-e2e", + "@test-parallel-ext" + ], + "lint": [ + "vendor/bin/pint" + ], + "format": [ + "php -d memory_limit=4G vendor/bin/pint" + ], + "check": [ + "vendor/bin/phpstan analyse src tests --level=max --memory-limit=4G" + ] + }, + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Appwrite Team", + "email": "team@appwrite.io" + } + ], + "description": "High-performance concurrent + parallel library with Promise and Parallel execution support for PHP.", + "support": { + "source": "https://github.com/utopia-php/async/tree/0.1.1", + "issues": "https://github.com/utopia-php/async/issues" + }, + "time": "2026-06-08T05:10:34+00:00" + }, { "name": "utopia-php/cache", - "version": "4.0.0", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "2ab50440114a7699b8a017ee2ecd8bcd9d54ac5e" + "reference": "92e02dab63606234b993b841ebf4c58845dd4620" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/2ab50440114a7699b8a017ee2ecd8bcd9d54ac5e", - "reference": "2ab50440114a7699b8a017ee2ecd8bcd9d54ac5e", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/92e02dab63606234b993b841ebf4c58845dd4620", + "reference": "92e02dab63606234b993b841ebf4c58845dd4620", "shasum": "" }, "require": { "ext-json": "*", - "ext-memcached": "*", - "ext-redis": "*", - "php": ">=8.3", - "utopia-php/circuit-breaker": "0.3.*", - "utopia-php/pools": "2.*", - "utopia-php/telemetry": "*" + "php": ">=8.4", + "utopia-php/circuit-breaker": "^0.3", + "utopia-php/pools": "^2.0", + "utopia-php/telemetry": "^0.4" }, "require-dev": { - "laravel/pint": "1.2.*", - "phpstan/phpstan": "^1.12", - "phpunit/phpunit": "^9.3", - "swoole/ide-helper": "^6.0", - "vimeo/psalm": "4.13.1" + "swoole/ide-helper": "^6.0" + }, + "suggest": { + "ext-memcached": "Required for the Memcached and Hazelcast adapters.", + "ext-redis": "Required for the Redis, RedisCluster and Sharding adapters.", + "ext-swoole": "Required for the multiplexing Redis adapter (>=6.0)." }, "type": "library", "autoload": { @@ -2240,6 +2422,12 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Team Appwrite", + "email": "team@appwrite.io" + } + ], "description": "A simple cache library to manage application cache storing, loading and purging", "keywords": [ "cache", @@ -2250,32 +2438,29 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/4.0.0" + "source": "https://github.com/utopia-php/cache/tree/4.0.2" }, - "time": "2026-07-31T13:04:45+00:00" + "time": "2026-08-12T07:48:59+00:00" }, { "name": "utopia-php/circuit-breaker", - "version": "0.3.1", + "version": "0.3.2", "source": { "type": "git", "url": "https://github.com/utopia-php/circuit-breaker.git", - "reference": "db5d77f6c99ebce2ee81bd8ed4ae8f41bd2b0828" + "reference": "5fbc3802471b0d1b4260bd9f5544514e6929b481" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/circuit-breaker/zipball/db5d77f6c99ebce2ee81bd8ed4ae8f41bd2b0828", - "reference": "db5d77f6c99ebce2ee81bd8ed4ae8f41bd2b0828", + "url": "https://api.github.com/repos/utopia-php/circuit-breaker/zipball/5fbc3802471b0d1b4260bd9f5544514e6929b481", + "reference": "5fbc3802471b0d1b4260bd9f5544514e6929b481", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "laravel/pint": "^1.29", - "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^10.0", - "utopia-php/telemetry": "^0.4" + "utopia-php/telemetry": "^0.4.6" }, "suggest": { "ext-opentelemetry": "Required by utopia-php/telemetry when using OpenTelemetry metrics.", @@ -2287,7 +2472,7 @@ "type": "library", "autoload": { "psr-4": { - "Utopia\\CircuitBreaker\\": "src/CircuitBreaker" + "Utopia\\CircuitBreaker\\": "src/CircuitBreaker/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2312,9 +2497,9 @@ ], "support": { "issues": "https://github.com/utopia-php/circuit-breaker/issues", - "source": "https://github.com/utopia-php/circuit-breaker/tree/0.3.1" + "source": "https://github.com/utopia-php/circuit-breaker/tree/0.3.2" }, - "time": "2026-05-29T12:12:23+00:00" + "time": "2026-08-05T18:07:20+00:00" }, { "name": "utopia-php/client", @@ -2423,16 +2608,16 @@ }, { "name": "utopia-php/database", - "version": "7.0.0", + "version": "dev-feat-query-lib", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "40185b100f92c402a189d6f4f0962c63bbe5726b" + "reference": "e593b78c587e1f1d66b644979f5c4ff11dffcd31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/40185b100f92c402a189d6f4f0962c63bbe5726b", - "reference": "40185b100f92c402a189d6f4f0962c63bbe5726b", + "url": "https://api.github.com/repos/utopia-php/database/zipball/e593b78c587e1f1d66b644979f5c4ff11dffcd31", + "reference": "e593b78c587e1f1d66b644979f5c4ff11dffcd31", "shasum": "" }, "require": { @@ -2441,29 +2626,67 @@ "ext-pdo": "*", "ext-redis": "*", "php": ">=8.5", - "utopia-php/cache": "^4.0.0", + "utopia-php/async": "0.1.*", + "utopia-php/cache": "4.*", "utopia-php/console": "0.1.*", "utopia-php/mongo": "1.*", "utopia-php/pools": "2.*", - "utopia-php/validators": "0.3.*" + "utopia-php/query": "0.4.*", + "utopia-php/validators": "0.4.*" }, "require-dev": { + "brianium/paratest": "7.20.*", "fakerphp/faker": "1.23.*", "laravel/pint": "*", - "pcov/clobber": "2.*", - "phpstan/phpstan": "1.*", - "phpunit/phpunit": "9.*", + "phpstan/phpstan": "2.1.*", + "phpunit/phpunit": "12.5.*", "rregeer/phpunit-coverage-check": "0.3.*", "swoole/ide-helper": "5.1.3", "utopia-php/cli": "0.22.*" }, + "suggest": { + "ext-pdo": "Needed to support MariaDB, MySQL or SQLite Database Adapter", + "ext-redis": "Needed to support Redis Cache Adapter", + "mongodb/mongodb": "Needed to support MongoDB Database Adapter" + }, "type": "library", "autoload": { "psr-4": { "Utopia\\Database\\": "src/Database" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "Tests\\E2E\\": "tests/e2e", + "Tests\\Unit\\": "tests/unit" + } + }, + "scripts": { + "build": [ + "Composer\\Config::disableProcessTimeout", + "docker compose build" + ], + "start": [ + "Composer\\Config::disableProcessTimeout", + "docker compose up -d" + ], + "test": [ + "Composer\\Config::disableProcessTimeout", + "docker compose exec tests vendor/bin/paratest --configuration phpunit.xml --functional --processes 4" + ], + "lint": [ + "php -d memory_limit=2G ./vendor/bin/pint --test" + ], + "format": [ + "php -d memory_limit=2G ./vendor/bin/pint" + ], + "check": [ + "./vendor/bin/phpstan analyse --memory-limit 2G" + ], + "coverage": [ + "./vendor/bin/coverage-check ./tmp/clover.xml 90" + ] + }, "license": [ "MIT" ], @@ -2476,10 +2699,10 @@ "utopia" ], "support": { - "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/7.0.0" + "source": "https://github.com/utopia-php/database/tree/feat-query-lib", + "issues": "https://github.com/utopia-php/database/issues" }, - "time": "2026-07-31T13:33:10+00:00" + "time": "2026-08-14T02:54:13+00:00" }, { "name": "utopia-php/dsn", @@ -2530,16 +2753,16 @@ }, { "name": "utopia-php/mongo", - "version": "1.4.0", + "version": "1.5.3", "source": { "type": "git", "url": "https://github.com/utopia-php/mongo.git", - "reference": "78818fd295f2829aaad5b74c9094e8c6f603550d" + "reference": "be29ee2d84b9f7efdfc6fea5b4b2d4bf95ff5ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/mongo/zipball/78818fd295f2829aaad5b74c9094e8c6f603550d", - "reference": "78818fd295f2829aaad5b74c9094e8c6f603550d", + "url": "https://api.github.com/repos/utopia-php/mongo/zipball/be29ee2d84b9f7efdfc6fea5b4b2d4bf95ff5ec4", + "reference": "be29ee2d84b9f7efdfc6fea5b4b2d4bf95ff5ec4", "shasum": "" }, "require": { @@ -2585,27 +2808,27 @@ ], "support": { "issues": "https://github.com/utopia-php/mongo/issues", - "source": "https://github.com/utopia-php/mongo/tree/1.4.0" + "source": "https://github.com/utopia-php/mongo/tree/1.5.3" }, - "time": "2026-07-30T05:24:58+00:00" + "time": "2026-08-13T01:55:11+00:00" }, { "name": "utopia-php/pools", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/utopia-php/pools.git", - "reference": "48905dac1b8f8050c2e07bdda32a018ca33982fc" + "reference": "86d43fcacd125232c0743e8c22695faf97285cf3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/pools/zipball/48905dac1b8f8050c2e07bdda32a018ca33982fc", - "reference": "48905dac1b8f8050c2e07bdda32a018ca33982fc", + "url": "https://api.github.com/repos/utopia-php/pools/zipball/86d43fcacd125232c0743e8c22695faf97285cf3", + "reference": "86d43fcacd125232c0743e8c22695faf97285cf3", "shasum": "" }, "require": { "php": ">=8.4", - "utopia-php/telemetry": "^0.4" + "utopia-php/telemetry": "^0.4.6" }, "require-dev": { "swoole/ide-helper": "6.*" @@ -2641,9 +2864,9 @@ ], "support": { "issues": "https://github.com/utopia-php/pools/issues", - "source": "https://github.com/utopia-php/pools/tree/2.0.1" + "source": "https://github.com/utopia-php/pools/tree/2.0.2" }, - "time": "2026-07-31T12:19:18+00:00" + "time": "2026-08-05T18:07:20+00:00" }, { "name": "utopia-php/psr7", @@ -2691,6 +2914,55 @@ }, "time": "2026-07-06T12:40:23+00:00" }, + { + "name": "utopia-php/query", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/query.git", + "reference": "c334515035a2ab0aa49176eeca96de3e1c096e22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/query/zipball/c334515035a2ab0aa49176eeca96de3e1c096e22", + "reference": "c334515035a2ab0aa49176eeca96de3e1c096e22", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "brianium/paratest": "*", + "laravel/pint": "*", + "mongodb/mongodb": "^2.0", + "phpstan/phpstan": "*", + "phpunit/phpcov": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Query\\": "src/Query" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple library providing a query abstraction for filtering, ordering, and pagination", + "keywords": [ + "framework", + "php", + "query", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/query/issues", + "source": "https://github.com/utopia-php/query/tree/0.4.0" + }, + "time": "2026-08-14T01:42:36+00:00" + }, { "name": "utopia-php/span", "version": "4.1.0", @@ -2733,16 +3005,16 @@ }, { "name": "utopia-php/storage", - "version": "4.0.1", + "version": "4.0.3", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "9684da5ab161ae9375d4f47541ef825451d69f2a" + "reference": "f8b122e753c01f30f774d390d7a36f030dfaef46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/9684da5ab161ae9375d4f47541ef825451d69f2a", - "reference": "9684da5ab161ae9375d4f47541ef825451d69f2a", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/f8b122e753c01f30f774d390d7a36f030dfaef46", + "reference": "f8b122e753c01f30f774d390d7a36f030dfaef46", "shasum": "" }, "require": { @@ -2754,8 +3026,8 @@ "psr/http-message": "^1.1 || ^2.0", "utopia-php/client": "0.2.* || 0.3.*", "utopia-php/psr7": "0.2.*", - "utopia-php/telemetry": "^0.4", - "utopia-php/validators": "0.3.*" + "utopia-php/telemetry": "^0.4.6", + "utopia-php/validators": "^0.4" }, "type": "library", "autoload": { @@ -2777,22 +3049,22 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/4.0.1" + "source": "https://github.com/utopia-php/storage/tree/4.0.3" }, - "time": "2026-07-31T09:20:57+00:00" + "time": "2026-08-10T04:51:03+00:00" }, { "name": "utopia-php/telemetry", - "version": "0.4.5", + "version": "0.4.6", "source": { "type": "git", "url": "https://github.com/utopia-php/telemetry.git", - "reference": "139943bffcd4f6dd8fb9ed247f946a1d151b006a" + "reference": "f96778a01792c32df0876fe1f38a79b9588445d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/telemetry/zipball/139943bffcd4f6dd8fb9ed247f946a1d151b006a", - "reference": "139943bffcd4f6dd8fb9ed247f946a1d151b006a", + "url": "https://api.github.com/repos/utopia-php/telemetry/zipball/f96778a01792c32df0876fe1f38a79b9588445d8", + "reference": "f96778a01792c32df0876fe1f38a79b9588445d8", "shasum": "" }, "require": { @@ -2828,22 +3100,22 @@ ], "support": { "issues": "https://github.com/utopia-php/telemetry/issues", - "source": "https://github.com/utopia-php/telemetry/tree/0.4.5" + "source": "https://github.com/utopia-php/telemetry/tree/0.4.6" }, - "time": "2026-07-08T11:07:25+00:00" + "time": "2026-08-05T17:56:48+00:00" }, { "name": "utopia-php/validators", - "version": "0.3.1", + "version": "0.4.2", "source": { "type": "git", "url": "https://github.com/utopia-php/validators.git", - "reference": "d9c2269ebd2596a09681ccd2fd133eeedfcdf9ba" + "reference": "a5b7b78b789e5af0a30f96da8355bbf48fb56699" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/validators/zipball/d9c2269ebd2596a09681ccd2fd133eeedfcdf9ba", - "reference": "d9c2269ebd2596a09681ccd2fd133eeedfcdf9ba", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/a5b7b78b789e5af0a30f96da8355bbf48fb56699", + "reference": "a5b7b78b789e5af0a30f96da8355bbf48fb56699", "shasum": "" }, "require": { @@ -2868,9 +3140,9 @@ ], "support": { "issues": "https://github.com/utopia-php/validators/issues", - "source": "https://github.com/utopia-php/validators/tree/0.3.1" + "source": "https://github.com/utopia-php/validators/tree/0.4.2" }, - "time": "2026-07-14T11:55:48+00:00" + "time": "2026-08-11T07:43:49+00:00" } ], "packages-dev": [ @@ -5148,10 +5420,19 @@ "time": "2026-07-06T19:11:50+00:00" } ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": {}, - "prefer-stable": false, + "aliases": [ + { + "package": "utopia-php/database", + "version": "dev-feat-query-lib", + "alias": "7.0.0", + "alias_normalized": "7.0.0.0" + } + ], + "minimum-stability": "dev", + "stability-flags": { + "utopia-php/database": 20 + }, + "prefer-stable": true, "prefer-lowest": false, "platform": { "php": ">=8.5", diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index a8eb59d0..f2ed2873 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -24,6 +24,9 @@ use Appwrite\Services\Teams; use Appwrite\Services\Users; use Override; +use Utopia\Database\Adapter\Feature\Spatial; +use Utopia\Database\Attribute as UtopiaAttribute; +use Utopia\Database\Capability; use Utopia\Database\Database as UtopiaDatabase; use Utopia\Database\DateTime; use Utopia\Database\Document as UtopiaDocument; @@ -35,7 +38,11 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; +use Utopia\Database\Index as UtopiaIndex; use Utopia\Database\Query; +use Utopia\Database\Relationship as UtopiaRelationship; +use Utopia\Database\RelationSide; +use Utopia\Database\RelationType; use Utopia\Database\Validator\Index as IndexValidator; use Utopia\Database\Validator\Structure; use Utopia\Database\Validator\UID; @@ -79,6 +86,9 @@ use Utopia\Migration\Resources\Storage\File; use Utopia\Migration\Resources\Templates\EmailTemplate; use Utopia\Migration\Transfer; +use Utopia\Query\Schema\ColumnType; +use Utopia\Query\Schema\ForeignKeyAction; +use Utopia\Query\Schema\IndexType; class Appwrite extends Destination { @@ -733,20 +743,10 @@ protected function createDatabase(Database $resource): bool try { $structure = $this->collectionStructureFor($resource); - $columns = \array_map( - fn ($attr) => new UtopiaDocument($attr), - $structure['attributes'] - ); - - $indexes = \array_map( - fn ($index) => new UtopiaDocument($index), - $structure['indexes'] - ); - $this->dbForProject->createCollection( $this->databaseCollectionId($existing), - $columns, - $indexes + $this->schemaAttributes($structure['attributes'] ?? []), + $this->schemaIndexes($structure['indexes'] ?? []) ); } catch (\Throwable $e) { $this->markDatabaseFailed($resource->getId()); @@ -794,20 +794,10 @@ protected function createDatabase(Database $resource): bool try { $structure = $this->collectionStructureFor($resource); - $columns = \array_map( - fn ($attr) => new UtopiaDocument($attr), - $structure['attributes'] - ); - - $indexes = \array_map( - fn ($index) => new UtopiaDocument($index), - $structure['indexes'] - ); - $this->dbForProject->createCollection( $this->databaseCollectionId($database), - $columns, - $indexes + $this->schemaAttributes($structure['attributes'] ?? []), + $this->schemaIndexes($structure['indexes'] ?? []) ); } catch (\Throwable $e) { // The metadata document exists but the database isn't usable; mark it failed before propagating. @@ -955,32 +945,7 @@ protected function createField(Column|Attribute $resource): bool } // column will be matching attribute as well // column type will be matching attribute type as well - $type = match ($resource->getType()) { - Column::TYPE_DATETIME => UtopiaDatabase::VAR_DATETIME, - Column::TYPE_BOOLEAN => UtopiaDatabase::VAR_BOOLEAN, - Column::TYPE_INTEGER => UtopiaDatabase::VAR_INTEGER, - Column::TYPE_BIG_INT => UtopiaDatabase::VAR_BIGINT, - Column::TYPE_FLOAT => UtopiaDatabase::VAR_FLOAT, - Column::TYPE_RELATIONSHIP => UtopiaDatabase::VAR_RELATIONSHIP, - - Column::TYPE_STRING, - Column::TYPE_IP, - Column::TYPE_EMAIL, - Column::TYPE_URL, - Column::TYPE_ENUM => UtopiaDatabase::VAR_STRING, - - Column::TYPE_POINT => UtopiaDatabase::VAR_POINT, - Column::TYPE_LINE => UtopiaDatabase::VAR_LINESTRING, - Column::TYPE_POLYGON => UtopiaDatabase::VAR_POLYGON, - Column::TYPE_TEXT => UtopiaDatabase::VAR_TEXT, - Column::TYPE_VARCHAR => UtopiaDatabase::VAR_VARCHAR, - Column::TYPE_MEDIUMTEXT => UtopiaDatabase::VAR_MEDIUMTEXT, - Column::TYPE_LONGTEXT => UtopiaDatabase::VAR_LONGTEXT, - Column::TYPE_OBJECT => UtopiaDatabase::VAR_OBJECT, - Column::TYPE_VECTOR => UtopiaDatabase::VAR_VECTOR, - - default => throw new \Exception('Invalid resource type ' . $resource->getType(), Exception::CODE_VALIDATION), - }; + $type = $this->schemaColumnType($resource); $database = $this->dbForProject->getDocument( self::META_DATABASES, @@ -1015,7 +980,7 @@ protected function createField(Column|Attribute $resource): bool } if (!empty($resource->getFormat())) { - if (!Structure::hasFormat($resource->getFormat(), $type)) { + if (!Structure::hasFormat($resource->getFormat(), ColumnType::from($type))) { $resource->setStatus(Resource::STATUS_ERROR, "Format {$resource->getFormat()} not available for column type {$type}"); $this->addError(new Exception( resourceName: $resource->getName(), @@ -1049,8 +1014,9 @@ protected function createField(Column|Attribute $resource): bool return false; } - if ($type === UtopiaDatabase::VAR_RELATIONSHIP) { - $resource->getOptions()['side'] = UtopiaDatabase::RELATION_SIDE_PARENT; + $relatedTable = null; + if ($type === ColumnType::Relationship->value) { + $resource->getOptions()['side'] = RelationSide::Parent->value; $relatedTable = $this->dbForProject->getDocument( $this->databaseCollectionId($database), $resource->getOptions()['relatedCollection'] @@ -1073,7 +1039,7 @@ protected function createField(Column|Attribute $resource): bool $this->trackOrphanCandidate($database, $table, 'attributeKeys', $resource->getKey(), $dbForDatabases); - $isRelationship = $type === UtopiaDatabase::VAR_RELATIONSHIP; + $isRelationship = $type === ColumnType::Relationship->value; // Source emits both sides of a two-way; processing one side reconciles both. Partner skip. $twoWayPairKey = $this->twoWayPairKey($database, $table, $resource, $type); @@ -1178,11 +1144,11 @@ protected function createField(Column|Attribute $resource): bool $twoWayKey = null; - if ($type === UtopiaDatabase::VAR_RELATIONSHIP && $options['twoWay']) { + if ($type === ColumnType::Relationship->value && $options['twoWay'] && $relatedTable !== null) { $twoWayKey = $options['twoWayKey']; $options['relatedCollection'] = $table->getId(); $options['twoWayKey'] = $resource->getKey(); - $options['side'] = UtopiaDatabase::RELATION_SIDE_CHILD; + $options['side'] = RelationSide::Child->value; try { $twoWayAttribute = new UtopiaDocument([ @@ -1241,16 +1207,25 @@ protected function createField(Column|Attribute $resource): bool try { switch ($type) { - case UtopiaDatabase::VAR_RELATIONSHIP: + case ColumnType::Relationship->value: + if ($relatedTable === null) { + throw new Exception( + resourceName: $resource->getName(), + resourceGroup: $resource->getGroup(), + resourceId: $resource->getId(), + message: 'Related table not found', + ); + } if (!$dbForDatabases->createRelationship( - collection: $this->tableCollectionId($database, $table), - // @phpstan-ignore-next-line — $relatedTable is set when type is VAR_RELATIONSHIP. - relatedCollection: $this->tableCollectionId($database, $relatedTable), - type: $options['relationType'], - twoWay: $options['twoWay'], - id: $resource->getKey(), - twoWayKey: $options['twoWay'] ? $twoWayKey : $options['twoWayKey'] ?? null, - onDelete: $options['onDelete'], + new UtopiaRelationship( + collection: $this->tableCollectionId($database, $table), + relatedCollection: $this->tableCollectionId($database, $relatedTable), + type: RelationType::from($options['relationType']), + twoWay: $options['twoWay'], + key: $resource->getKey(), + twoWayKey: (string) ($options['twoWay'] ? $twoWayKey : $options['twoWayKey'] ?? ''), + onDelete: ForeignKeyAction::from($options['onDelete']), + ) )) { throw new Exception( resourceName: $resource->getName(), @@ -1263,16 +1238,18 @@ protected function createField(Column|Attribute $resource): bool default: if (!$dbForDatabases->createAttribute( $this->tableCollectionId($database, $table), - $resource->getKey(), - $type, - $resource->getSize(), - $resource->isRequired(), - $resource->getDefault(), - $resource->isSigned(), - $resource->isArray(), - $resource->getFormat(), - $resource->getFormatOptions(), - $resource->getFilters(), + new UtopiaAttribute( + key: $resource->getKey(), + type: ColumnType::from($type), + size: $resource->getSize(), + required: $resource->isRequired(), + default: $resource->getDefault(), + signed: $resource->isSigned(), + array: $resource->isArray(), + format: $resource->getFormat() !== '' ? $resource->getFormat() : null, + formatOptions: $resource->getFormatOptions(), + filters: $resource->getFilters(), + ), )) { throw new \Exception('Failed to create Column', Exception::CODE_INTERNAL); } @@ -1287,8 +1264,7 @@ protected function createField(Column|Attribute $resource): bool throw $e; } - if ($type === UtopiaDatabase::VAR_RELATIONSHIP && $options['twoWay']) { - // @phpstan-ignore-next-line — $relatedTable is set when type is VAR_RELATIONSHIP. + if ($type === ColumnType::Relationship->value && $options['twoWay'] && $relatedTable !== null) { $this->dbForProject->purgeCachedDocument($this->databaseCollectionId($database), $relatedTable->getId()); } @@ -1311,6 +1287,69 @@ private function collectionStructureFor(Database $resource): array return $this->collectionStructures[$resource->getType()] ?? $this->collectionStructure; } + private function schemaColumnType(Column|Attribute $resource): string + { + return match ($resource->getType()) { + Column::TYPE_DATETIME => ColumnType::Datetime->value, + Column::TYPE_BOOLEAN => ColumnType::Boolean->value, + Column::TYPE_INTEGER => ColumnType::Integer->value, + Column::TYPE_BIG_INT => ColumnType::BigInteger->value, + Column::TYPE_FLOAT => ColumnType::Double->value, + Column::TYPE_RELATIONSHIP => ColumnType::Relationship->value, + Column::TYPE_STRING, + Column::TYPE_IP, + Column::TYPE_EMAIL, + Column::TYPE_URL, + Column::TYPE_ENUM => ColumnType::String->value, + Column::TYPE_POINT => ColumnType::Point->value, + Column::TYPE_LINE => ColumnType::Linestring->value, + Column::TYPE_POLYGON => ColumnType::Polygon->value, + Column::TYPE_TEXT => ColumnType::Text->value, + Column::TYPE_VARCHAR => ColumnType::Varchar->value, + Column::TYPE_MEDIUMTEXT => ColumnType::MediumText->value, + Column::TYPE_LONGTEXT => ColumnType::LongText->value, + Column::TYPE_OBJECT => ColumnType::Object->value, + Column::TYPE_VECTOR => ColumnType::Vector->value, + default => throw new \Exception('Invalid resource type ' . $resource->getType(), Exception::CODE_VALIDATION), + }; + } + + /** + * @param array $attributes + * @return array + */ + private function schemaAttributes(array $attributes): array + { + return \array_map(function (mixed $attr): UtopiaAttribute { + if ($attr instanceof UtopiaAttribute) { + return $attr; + } + if ($attr instanceof UtopiaDocument) { + return UtopiaAttribute::fromDocument($attr); + } + + return UtopiaAttribute::fromArray(\is_array($attr) ? $attr : []); + }, $attributes); + } + + /** + * @param array $indexes + * @return array + */ + private function schemaIndexes(array $indexes): array + { + return \array_map(function (mixed $index): UtopiaIndex { + if ($index instanceof UtopiaIndex) { + return $index; + } + if ($index instanceof UtopiaDocument) { + return UtopiaIndex::fromDocument($index); + } + + return UtopiaIndex::fromArray(\is_array($index) ? $index : []); + }, $indexes); + } + /** * Type-specific metadata for an imported entity's collection document. VectorsDB collections * carry a required `dimension`; archives written before it was serialized have none, so a @@ -1342,7 +1381,7 @@ private function entityTypeMetadata(Table $resource): array private function syncVectorDimension(Column|Attribute $resource, string $type, UtopiaDocument $database, UtopiaDocument $table): void { if ( - $type !== UtopiaDatabase::VAR_VECTOR + $type !== ColumnType::Vector->value || $resource->getKey() !== self::VECTORSDB_EMBEDDINGS_KEY || $resource->getTable()->getDatabase()->getType() !== Resource::TYPE_DATABASE_VECTORSDB || !isset($this->collectionStructures[Resource::TYPE_DATABASE_VECTORSDB]) @@ -1461,7 +1500,7 @@ protected function createIndex(Index $resource): bool // Lengths hidden by default $lengths = []; - if ($dbForDatabases->getAdapter()->getSupportForAttributes()) { + if ($dbForDatabases->getAdapter()->supports(Capability::DefinedAttributes)) { $this->validateFieldsForIndexes($resource, $table, $lengths); } @@ -1487,24 +1526,27 @@ protected function createIndex(Index $resource): bool $tableColumns = $table->getAttribute('attributes', []); $tableIndexes = $table->getAttribute('indexes', []); + $adapter = $dbForDatabases->getAdapter(); $validator = new IndexValidator( $tableColumns, $tableIndexes, - $dbForDatabases->getAdapter()->getMaxIndexLength(), - $dbForDatabases->getAdapter()->getInternalIndexesKeys(), - $dbForDatabases->getAdapter()->getSupportForIndexArray(), - $dbForDatabases->getAdapter()->getSupportForSpatialIndexNull(), - $dbForDatabases->getAdapter()->getSupportForSpatialIndexOrder(), - $dbForDatabases->getAdapter()->getSupportForVectors(), - $dbForDatabases->getAdapter()->getSupportForAttributes(), - $dbForDatabases->getAdapter()->getSupportForMultipleFulltextIndexes(), - $dbForDatabases->getAdapter()->getSupportForIdenticalIndexes(), - $dbForDatabases->getAdapter()->getSupportForObjectIndexes(), - $dbForDatabases->getAdapter()->getSupportForTrigramIndex(), - $dbForDatabases->getAdapter()->getSupportForSpatialAttributes(), - $dbForDatabases->getAdapter()->getSupportForIndex(), - $dbForDatabases->getAdapter()->getSupportForUniqueIndex(), - $dbForDatabases->getAdapter()->getSupportForFulltextIndex() + $adapter->getMaxIndexLength(), + $adapter->getInternalIndexesKeys(), + $adapter->supports(Capability::IndexArray), + $adapter->supports(Capability::SpatialIndexNull), + $adapter->supports(Capability::SpatialIndexOrder), + $adapter->supports(Capability::Vectors), + $adapter->supports(Capability::DefinedAttributes), + $adapter->supports(Capability::MultipleFulltextIndexes), + $adapter->supports(Capability::IdenticalIndexes), + $adapter->supports(Capability::ObjectIndexes), + $adapter->supports(Capability::TrigramIndex), + $adapter instanceof Spatial, + $adapter->supports(Capability::Index), + $adapter->supports(Capability::UniqueIndex), + $adapter->supports(Capability::Fulltext), + $adapter->supports(Capability::TTLIndexes), + $adapter->supports(Capability::Objects), ); if (!$validator->isValid($index)) { @@ -1523,11 +1565,13 @@ protected function createIndex(Index $resource): bool try { $result = $dbForDatabases->createIndex( $this->tableCollectionId($database, $table), - $resource->getKey(), - $resource->getType(), - $resource->getColumns(), - $lengths, - $resource->getOrders() + new UtopiaIndex( + key: $resource->getKey(), + type: IndexType::from($resource->getType()), + attributes: $resource->getColumns(), + lengths: $lengths, + orders: $resource->getOrders(), + ), ); if (!$result) { @@ -1642,17 +1686,19 @@ protected function createRecord(Row $resource, bool $isLast): bool $resource->getTable()->getId(), ); // Strip row payload fields the table doesn't declare — guards against orphans surviving in source archives. - if ($dbForDatabases->getAdapter()->getSupportForAttributes()) { + if ($dbForDatabases->getAdapter()->supports(Capability::DefinedAttributes)) { foreach ($this->rowBuffer as $row) { foreach ($row as $key => $value) { if (\str_starts_with($key, '$')) { continue; } - /** @var \Utopia\Database\Document $attribute */ $found = false; foreach ($table->getAttribute('attributes', []) as $attribute) { - if ($attribute->getAttribute('key') == $key) { + $attrKey = $attribute instanceof UtopiaAttribute + ? $attribute->key + : $attribute->getAttribute('key'); + if ($attrKey == $key) { $found = true; break; } @@ -1710,7 +1756,7 @@ protected function createRecord(Row $resource, bool $isLast): bool return true; } - /** Relationships route through deleteRelationship since deleteAttribute throws for VAR_RELATIONSHIP. */ + /** Relationships route through deleteRelationship since deleteAttribute throws for relationship columns. */ private function dropAttributeForRecreate( UtopiaDocument $database, UtopiaDocument $table, @@ -1833,7 +1879,7 @@ private function updateRelationshipInPlace( $dbForDatabases->updateRelationship( collection: $this->tableCollectionId($database, $table), id: $resource->getKey(), - onDelete: (string) ($sourceOptions['onDelete'] ?? ''), + onDelete: ForeignKeyAction::from((string) ($sourceOptions['onDelete'] ?? ForeignKeyAction::Restrict->value)), ); } @@ -2021,7 +2067,7 @@ private function twoWayPairKey( Column|Attribute $resource, string $type, ): ?string { - if ($type !== UtopiaDatabase::VAR_RELATIONSHIP) { + if ($type !== ColumnType::Relationship->value) { return null; } $options = $resource->getOptions(); @@ -2201,7 +2247,7 @@ private function dropOrphanAttribute( $options = $attrDoc->getAttribute('options', []); $collectionId = $this->tableCollectionId($database, $table); - if ($type === UtopiaDatabase::VAR_RELATIONSHIP) { + if ($type === ColumnType::Relationship->value) { $this->bestEffort(fn () => $dbForDatabases->deleteRelationship($collectionId, $key)); } else { $this->bestEffort(fn () => $dbForDatabases->deleteAttribute($collectionId, $key)); @@ -2210,7 +2256,7 @@ private function dropOrphanAttribute( $this->dbForProject->purgeCachedDocument($this->databaseCollectionId($database), $table->getId()); $dbForDatabases->purgeCachedCollection($collectionId); - if ($type !== UtopiaDatabase::VAR_RELATIONSHIP) { + if ($type !== ColumnType::Relationship->value) { return; } $partner = $this->resolveTwoWayPartner($database, $options); @@ -3859,13 +3905,19 @@ private function validateFieldsForIndexes(Index $resource, UtopiaDocument $table $tableColumns = $table->getAttribute('attributes', []); $oldColumns = \array_map( - fn ($attr) => $attr->getArrayCopy(), + function ($attr) { + if ($attr instanceof UtopiaAttribute) { + return $attr->toDocument()->getArrayCopy(); + } + + return $attr->getArrayCopy(); + }, $tableColumns ); $oldColumns[] = [ 'key' => '$id', - 'type' => UtopiaDatabase::VAR_STRING, + 'type' => ColumnType::String->value, 'status' => 'available', 'required' => true, 'array' => false, @@ -3875,7 +3927,7 @@ private function validateFieldsForIndexes(Index $resource, UtopiaDocument $table $oldColumns[] = [ 'key' => '$createdAt', - 'type' => UtopiaDatabase::VAR_DATETIME, + 'type' => ColumnType::Datetime->value, 'status' => 'available', 'signed' => false, 'required' => false, @@ -3886,7 +3938,7 @@ private function validateFieldsForIndexes(Index $resource, UtopiaDocument $table $oldColumns[] = [ 'key' => '$updatedAt', - 'type' => UtopiaDatabase::VAR_DATETIME, + 'type' => ColumnType::Datetime->value, 'status' => 'available', 'signed' => false, 'required' => false, @@ -3915,7 +3967,7 @@ private function validateFieldsForIndexes(Index $resource, UtopiaDocument $table $columnType = $oldColumns[$columnIndex]['type']; $columnArray = $oldColumns[$columnIndex]['array'] ?? false; - if ($columnType === UtopiaDatabase::VAR_RELATIONSHIP) { + if ($columnType === ColumnType::Relationship->value || $columnType === ColumnType::Relationship) { throw new Exception( resourceName: $resource->getName(), resourceGroup: $resource->getGroup(), diff --git a/src/Migration/Resources/Database/Columns/Relationship.php b/src/Migration/Resources/Database/Columns/Relationship.php index b530c317..6cf1c736 100644 --- a/src/Migration/Resources/Database/Columns/Relationship.php +++ b/src/Migration/Resources/Database/Columns/Relationship.php @@ -2,9 +2,10 @@ namespace Utopia\Migration\Resources\Database\Columns; -use Utopia\Database\Database; +use Utopia\Database\RelationSide; use Utopia\Migration\Resources\Database\Column; use Utopia\Migration\Resources\Database\Table; +use Utopia\Query\Schema\ForeignKeyAction; class Relationship extends Column { @@ -15,8 +16,8 @@ public function __construct( string $relationType, bool $twoWay = false, ?string $twoWayKey = null, - string $onDelete = Database::RELATION_MUTATE_RESTRICT, - string $side = Database::RELATION_SIDE_PARENT, + string $onDelete = ForeignKeyAction::Restrict->value, + string $side = RelationSide::Parent->value, string $createdAt = '', string $updatedAt = '' ) { diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 61443459..34ed9a54 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -22,6 +22,7 @@ use Utopia\Database\Database as UtopiaDatabase; use Utopia\Database\DateTime as UtopiaDateTime; use Utopia\Database\Document as UtopiaDocument; +use Utopia\Database\RelationSide; use Utopia\Migration\Exception; use Utopia\Migration\Resource; use Utopia\Migration\Resources\Auth\AuthMethods; @@ -88,6 +89,7 @@ use Utopia\Migration\Sources\Appwrite\Reader\API as APIReader; use Utopia\Migration\Sources\Appwrite\Reader\Database as DatabaseReader; use Utopia\Migration\Transfer; +use Utopia\Query\Schema\ColumnType; class Appwrite extends Source { @@ -1283,8 +1285,8 @@ private function exportFields(string $entityType, int $batchSize): void foreach ($response as $column) { if ( - $column['type'] === UtopiaDatabase::VAR_RELATIONSHIP - && $column['side'] === UtopiaDatabase::RELATION_SIDE_CHILD + $column['type'] === ColumnType::Relationship->value + && $column['side'] === RelationSide::Child->value ) { continue; } diff --git a/src/Migration/Sources/Appwrite/Reader/Database.php b/src/Migration/Sources/Appwrite/Reader/Database.php index 547ec1e6..701eafdb 100644 --- a/src/Migration/Sources/Appwrite/Reader/Database.php +++ b/src/Migration/Sources/Appwrite/Reader/Database.php @@ -2,6 +2,7 @@ namespace Utopia\Migration\Sources\Appwrite\Reader; +use Utopia\Database\Capability; use Utopia\Database\Database as UtopiaDatabase; use Utopia\Database\Document as UtopiaDocument; use Utopia\Database\Exception as DatabaseException; @@ -16,6 +17,7 @@ use Utopia\Migration\Resources\Database\Row as RowResource; use Utopia\Migration\Resources\Database\Table as TableResource; use Utopia\Migration\Sources\Appwrite\Reader; +use Utopia\Query\Schema\ColumnType; /** * @implements Reader @@ -259,13 +261,18 @@ public function listColumns(TableResource $resource, array $queries = []): array } foreach ($columns as $column) { - if ($column['type'] !== UtopiaDatabase::VAR_RELATIONSHIP) { + if ($column['type'] !== ColumnType::Relationship->value) { continue; } $options = $column['options']; - foreach ($options as $key => $value) { - $column[$key] = $value; + if ($options instanceof UtopiaDocument) { + $options = $options->getArrayCopy(); + } + if (\is_array($options)) { + foreach ($options as $key => $value) { + $column[$key] = $value; + } } unset($column['options']); @@ -492,7 +499,7 @@ public function queryLimit(int $limit): Query public function getSupportForAttributes(): bool { - return $this->dbForProject->getAdapter()->getSupportForAttributes(); + return $this->dbForProject->getAdapter()->supports(Capability::DefinedAttributes); } /** diff --git a/src/Migration/Sources/CSV.php b/src/Migration/Sources/CSV.php index 0b6b725c..5ca8ae49 100644 --- a/src/Migration/Sources/CSV.php +++ b/src/Migration/Sources/CSV.php @@ -3,6 +3,8 @@ namespace Utopia\Migration\Sources; use Utopia\Database\Database as UtopiaDatabase; +use Utopia\Database\RelationSide; +use Utopia\Database\RelationType; use Utopia\Migration\Exception; use Utopia\Migration\Resource; use Utopia\Migration\Resource as UtopiaResource; @@ -232,7 +234,7 @@ private function exportRows(int $batchSize): void if ( $type === Column::TYPE_RELATIONSHIP && - $relationSide === UtopiaDatabase::RELATION_SIDE_CHILD + $relationSide === RelationSide::Child->value ) { continue; } @@ -244,8 +246,8 @@ private function exportRows(int $batchSize): void if ( $type === Column::TYPE_RELATIONSHIP && - $relationType === UtopiaDatabase::RELATION_MANY_TO_MANY && - $relationSide === UtopiaDatabase::RELATION_SIDE_PARENT + $relationType === RelationType::ManyToMany->value && + $relationSide === RelationSide::Parent->value ) { $manyToManyKeys[$key] = true; } diff --git a/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php b/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php index 1e125acd..00f34bdf 100644 --- a/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php +++ b/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php @@ -7,6 +7,7 @@ use Utopia\Cache\Adapter\Memory as MemoryCache; use Utopia\Cache\Cache; use Utopia\Database\Adapter\Memory as MemoryAdapter; +use Utopia\Database\Attribute as UtopiaAttribute; use Utopia\Database\Database as UtopiaDatabase; use Utopia\Database\Document as UtopiaDocument; use Utopia\Migration\Destinations\Appwrite as AppwriteDestination; @@ -14,6 +15,7 @@ use Utopia\Migration\Resource; use Utopia\Migration\Resources\Database\Database as DatabaseResource; use Utopia\Migration\Transfer; +use Utopia\Query\Schema\ColumnType; use Utopia\Tests\Unit\Adapters\MockSource; class CountingAppwriteDestination extends AppwriteDestination @@ -76,16 +78,16 @@ private function createProjectDatabase(bool $withStatus): UtopiaDatabase $database->create(); $attributes = [ - $this->attribute('name', UtopiaDatabase::VAR_STRING, required: true, size: 256), - $this->attribute('enabled', UtopiaDatabase::VAR_BOOLEAN, default: true), - $this->attribute('search', UtopiaDatabase::VAR_STRING, size: 16384), - $this->attribute('originalId', UtopiaDatabase::VAR_STRING, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('type', UtopiaDatabase::VAR_STRING, default: 'tablesdb', size: 128), - $this->attribute('database', UtopiaDatabase::VAR_STRING, size: 2000), + $this->attribute('name', ColumnType::String, required: true, size: 256), + $this->attribute('enabled', ColumnType::Boolean, default: true), + $this->attribute('search', ColumnType::String, size: 16384), + $this->attribute('originalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('type', ColumnType::String, default: 'tablesdb', size: 128), + $this->attribute('database', ColumnType::String, size: 2000), ]; if ($withStatus) { - $attributes[] = $this->attribute('status', UtopiaDatabase::VAR_STRING, size: 16); + $attributes[] = $this->attribute('status', ColumnType::String, size: 16); } $database->createCollection('databases', $attributes); @@ -95,21 +97,18 @@ private function createProjectDatabase(bool $withStatus): UtopiaDatabase private function attribute( string $id, - string $type, + ColumnType $type, bool $required = false, mixed $default = null, int $size = 0, - ): UtopiaDocument { - return new UtopiaDocument([ - '$id' => $id, - 'type' => $type, - 'size' => $size, - 'required' => $required, - 'default' => $default, - 'array' => false, - 'signed' => true, - 'filters' => [], - ]); + ): UtopiaAttribute { + return new UtopiaAttribute( + key: $id, + type: $type, + size: $size, + required: $required, + default: $default, + ); } private function runDatabaseTransfer(UtopiaDatabase $database, bool $explicit): CountingAppwriteDestination diff --git a/tests/Migration/Unit/Destinations/AppwriteIndexLengthsTest.php b/tests/Migration/Unit/Destinations/AppwriteIndexLengthsTest.php index 362f6e79..e7a70388 100644 --- a/tests/Migration/Unit/Destinations/AppwriteIndexLengthsTest.php +++ b/tests/Migration/Unit/Destinations/AppwriteIndexLengthsTest.php @@ -8,6 +8,7 @@ use Utopia\Cache\Adapter\Memory as MemoryCache; use Utopia\Cache\Cache; use Utopia\Database\Adapter\Memory as MemoryAdapter; +use Utopia\Database\Attribute as UtopiaAttribute; use Utopia\Database\Database as UtopiaDatabase; use Utopia\Database\Document as UtopiaDocument; use Utopia\Migration\Destinations\Appwrite as AppwriteDestination; @@ -18,6 +19,7 @@ use Utopia\Migration\Resources\Database\Index; use Utopia\Migration\Resources\Database\Table; use Utopia\Migration\Transfer; +use Utopia\Query\Schema\ColumnType; use Utopia\Tests\Unit\Adapters\MockSource; /** @@ -188,14 +190,14 @@ private function transferIndex( getDatabasesDB: static fn (UtopiaDocument $document): UtopiaDatabase => $database, collectionStructure: [ 'attributes' => [ - $this->attributeArray('databaseInternalId', UtopiaDatabase::VAR_STRING, size: UtopiaDatabase::LENGTH_KEY), - $this->attributeArray('databaseId', UtopiaDatabase::VAR_STRING, size: UtopiaDatabase::LENGTH_KEY), - $this->attributeArray('name', UtopiaDatabase::VAR_STRING, size: 256), - $this->attributeArray('enabled', UtopiaDatabase::VAR_BOOLEAN, default: true), - $this->attributeArray('documentSecurity', UtopiaDatabase::VAR_BOOLEAN, default: false), - $this->attributeArray('search', UtopiaDatabase::VAR_STRING, size: 16384), - $this->attributeArray('attributes', UtopiaDatabase::VAR_STRING, size: 16384, filters: ['subQueryAttributes']), - $this->attributeArray('indexes', UtopiaDatabase::VAR_STRING, size: 16384, filters: ['subQueryIndexes']), + $this->attributeArray('databaseInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attributeArray('databaseId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attributeArray('name', ColumnType::String, size: 256), + $this->attributeArray('enabled', ColumnType::Boolean, default: true), + $this->attributeArray('documentSecurity', ColumnType::Boolean, default: false), + $this->attributeArray('search', ColumnType::String, size: 16384), + $this->attributeArray('attributes', ColumnType::String, size: 16384, filters: ['subQueryAttributes']), + $this->attributeArray('indexes', ColumnType::String, size: 16384, filters: ['subQueryIndexes']), ], 'indexes' => [], ], @@ -245,46 +247,46 @@ private function projectDatabase(): UtopiaDatabase $database->create(); $database->createCollection('databases', [ - $this->attribute('name', UtopiaDatabase::VAR_STRING, required: true, size: 256), - $this->attribute('enabled', UtopiaDatabase::VAR_BOOLEAN, default: true), - $this->attribute('search', UtopiaDatabase::VAR_STRING, size: 16384), - $this->attribute('originalId', UtopiaDatabase::VAR_STRING, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('type', UtopiaDatabase::VAR_STRING, default: 'tablesdb', size: 128), - $this->attribute('database', UtopiaDatabase::VAR_STRING, size: 2000), + $this->attribute('name', ColumnType::String, required: true, size: 256), + $this->attribute('enabled', ColumnType::Boolean, default: true), + $this->attribute('search', ColumnType::String, size: 16384), + $this->attribute('originalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('type', ColumnType::String, default: 'tablesdb', size: 128), + $this->attribute('database', ColumnType::String, size: 2000), ]); $database->createCollection('attributes', [ - $this->attribute('key', UtopiaDatabase::VAR_STRING, size: 256), - $this->attribute('databaseInternalId', UtopiaDatabase::VAR_STRING, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('databaseId', UtopiaDatabase::VAR_STRING, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('collectionInternalId', UtopiaDatabase::VAR_STRING, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('collectionId', UtopiaDatabase::VAR_STRING, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('type', UtopiaDatabase::VAR_STRING, size: 256), - $this->attribute('status', UtopiaDatabase::VAR_STRING, size: 64), - $this->attribute('size', UtopiaDatabase::VAR_INTEGER), - $this->attribute('required', UtopiaDatabase::VAR_BOOLEAN, default: false), - $this->attribute('signed', UtopiaDatabase::VAR_BOOLEAN, default: true), - $this->attribute('default', UtopiaDatabase::VAR_STRING, size: 16384), - $this->attribute('array', UtopiaDatabase::VAR_BOOLEAN, default: false), - $this->attribute('format', UtopiaDatabase::VAR_STRING, size: 64), - $this->attribute('formatOptions', UtopiaDatabase::VAR_STRING, size: 16384, filters: ['json']), - $this->attribute('filters', UtopiaDatabase::VAR_STRING, size: 64, array: true), - $this->attribute('options', UtopiaDatabase::VAR_STRING, size: 16384, filters: ['json']), - $this->attribute('error', UtopiaDatabase::VAR_STRING, size: 2048), + $this->attribute('key', ColumnType::String, size: 256), + $this->attribute('databaseInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('databaseId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('collectionInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('collectionId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('type', ColumnType::String, size: 256), + $this->attribute('status', ColumnType::String, size: 64), + $this->attribute('size', ColumnType::Integer), + $this->attribute('required', ColumnType::Boolean, default: false), + $this->attribute('signed', ColumnType::Boolean, default: true), + $this->attribute('default', ColumnType::String, size: 16384), + $this->attribute('array', ColumnType::Boolean, default: false), + $this->attribute('format', ColumnType::String, size: 64), + $this->attribute('formatOptions', ColumnType::String, size: 16384, filters: ['json']), + $this->attribute('filters', ColumnType::String, size: 64, array: true), + $this->attribute('options', ColumnType::String, size: 16384, filters: ['json']), + $this->attribute('error', ColumnType::String, size: 2048), ]); $database->createCollection('indexes', [ - $this->attribute('key', UtopiaDatabase::VAR_STRING, size: 256), - $this->attribute('status', UtopiaDatabase::VAR_STRING, size: 64), - $this->attribute('databaseInternalId', UtopiaDatabase::VAR_STRING, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('databaseId', UtopiaDatabase::VAR_STRING, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('collectionInternalId', UtopiaDatabase::VAR_STRING, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('collectionId', UtopiaDatabase::VAR_STRING, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('type', UtopiaDatabase::VAR_STRING, size: 16), - $this->attribute('attributes', UtopiaDatabase::VAR_STRING, size: 256, array: true), - $this->attribute('lengths', UtopiaDatabase::VAR_INTEGER, array: true), - $this->attribute('orders', UtopiaDatabase::VAR_STRING, size: 4, array: true), - $this->attribute('error', UtopiaDatabase::VAR_STRING, size: 2048), + $this->attribute('key', ColumnType::String, size: 256), + $this->attribute('status', ColumnType::String, size: 64), + $this->attribute('databaseInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('databaseId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('collectionInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('collectionId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('type', ColumnType::String, size: 16), + $this->attribute('attributes', ColumnType::String, size: 256, array: true), + $this->attribute('lengths', ColumnType::Integer, array: true), + $this->attribute('orders', ColumnType::String, size: 4, array: true), + $this->attribute('error', ColumnType::String, size: 2048), ]); return $database; @@ -296,7 +298,7 @@ private function projectDatabase(): UtopiaDatabase */ private function attributeArray( string $id, - string $type, + ColumnType $type, bool $required = false, mixed $default = null, int $size = 0, @@ -305,7 +307,7 @@ private function attributeArray( ): array { return [ '$id' => $id, - 'type' => $type, + 'type' => $type->value, 'size' => $size, 'required' => $required, 'default' => $default, @@ -320,14 +322,22 @@ private function attributeArray( */ private function attribute( string $id, - string $type, + ColumnType $type, bool $required = false, mixed $default = null, int $size = 0, bool $array = false, array $filters = [], - ): UtopiaDocument { - return new UtopiaDocument($this->attributeArray($id, $type, $required, $default, $size, $array, $filters)); + ): UtopiaAttribute { + return new UtopiaAttribute( + key: $id, + type: $type, + size: $size, + required: $required, + default: $default, + array: $array, + filters: $filters, + ); } /** From d32d19600d8f4b16e9ed8bc697f7ce195151d2b6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 14 Aug 2026 23:55:54 +1200 Subject: [PATCH 02/14] fix: accept query-lib biginteger as API bigint Appwrite stores ColumnType::BigInteger as biginteger. CSV export resolved that as an unsupported column type and wrote no rows. --- src/Migration/Resources/Database/Column.php | 4 ++++ tests/Migration/Unit/Resources/ColumnTest.php | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/Migration/Resources/Database/Column.php b/src/Migration/Resources/Database/Column.php index cc056504..a9df216f 100644 --- a/src/Migration/Resources/Database/Column.php +++ b/src/Migration/Resources/Database/Column.php @@ -86,6 +86,10 @@ public static function resolve(array $column): array $type = \is_string($column['type'] ?? null) ? $column['type'] : ''; $format = \is_string($column['format'] ?? null) ? $column['format'] : ''; + if ($type === 'biginteger') { + $type = self::TYPE_BIG_INT; + } + if (isset(self::FORMAT_SIZES[$type])) { $format = $type; $type = self::TYPE_STRING; diff --git a/tests/Migration/Unit/Resources/ColumnTest.php b/tests/Migration/Unit/Resources/ColumnTest.php index 045f155e..fc2d7967 100644 --- a/tests/Migration/Unit/Resources/ColumnTest.php +++ b/tests/Migration/Unit/Resources/ColumnTest.php @@ -106,4 +106,12 @@ public function testUnsizedAndUnknownDefinitionsResolveToZero(): void Column::resolve(['key' => 'unknown']), ); } + + public function testQueryLibBigIntegerAliasBecomesBigint(): void + { + $this->assertSame( + ['type' => Column::TYPE_BIG_INT, 'format' => '', 'size' => 0], + Column::resolve(['key' => 'total', 'type' => 'biginteger']), + ); + } } From 47e2597bc204c60c920891295a88bfa8e025743f Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 15 Aug 2026 01:47:48 +1200 Subject: [PATCH 03/14] fix: create collection metadata from the persisted database sequence createDocument can return an empty Mongo sequence while a subsequent getDocument has the ObjectId. Creating database_{seq} from the create return left table import looking up a collection that did not exist. --- src/Migration/Destinations/Appwrite.php | 5 +++++ .../Unit/Destinations/AppwriteDatabaseStatusTest.php | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index f2ed2873..75fb2219 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -788,6 +788,11 @@ protected function createDatabase(Database $resource): bool } $database = $this->dbForProject->createDocument(self::META_DATABASES, new UtopiaDocument($document)); + $database = $this->dbForProject->getDocument(self::META_DATABASES, $database->getId()); + + if ($database->isEmpty()) { + throw new DatabaseException('Failed to reload created database '.$resource->getId()); + } $resource->setSequence($database->getSequence()); diff --git a/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php b/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php index 00f34bdf..bde31198 100644 --- a/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php +++ b/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php @@ -48,6 +48,10 @@ public function testDatabaseCreationOmitsStatusThroughLegacyAndExplicitEntrypoin $this->assertSame(1, $destination->runCount); $this->assertFalse($created->isEmpty()); $this->assertArrayNotHasKey('status', $created->getArrayCopy()); + $this->assertFalse( + $database->getCollection('database_'.$created->getSequence())->isEmpty(), + 'Metadata collection must use the persisted database sequence', + ); } } @@ -63,6 +67,10 @@ public function testDatabaseCreationPreservesLifecycleThroughLegacyAndExplicitEn $this->assertSame(1, $destination->runCount); $this->assertFalse($created->isEmpty()); $this->assertSame('ready', $created->getAttribute('status')); + $this->assertFalse( + $database->getCollection('database_'.$created->getSequence())->isEmpty(), + 'Metadata collection must use the persisted database sequence', + ); } } From cce7c66ae91c6639290f5050f11e9264c8da033b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 14:09:35 +1200 Subject: [PATCH 04/14] (fix): allow migrating HuggingFace OAuth2 providers Appwrite main added huggingface as a project OAuth2 provider. Without an allow-list entry, Appwrite-to-Appwrite migrations fail on that provider even when the rest of the transfer succeeded. --- .../Resources/Auth/OAuth2/OAuth2Provider.php | 1 + .../Unit/Resources/OAuth2ProviderTest.php | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Migration/Resources/Auth/OAuth2/OAuth2Provider.php b/src/Migration/Resources/Auth/OAuth2/OAuth2Provider.php index e029dbdf..54c2ee00 100644 --- a/src/Migration/Resources/Auth/OAuth2/OAuth2Provider.php +++ b/src/Migration/Resources/Auth/OAuth2/OAuth2Provider.php @@ -49,6 +49,7 @@ final class OAuth2Provider extends Resource 'github' => ['clientId' => ['target' => self::TARGET_APP_ID]], 'gitlab' => ['clientId' => ['target' => self::TARGET_APP_ID], 'endpoint' => ['target' => self::TARGET_SECRET]], 'google' => ['clientId' => ['target' => self::TARGET_APP_ID], 'prompt' => ['target' => self::TARGET_SECRET]], + 'huggingface' => ['clientId' => ['target' => self::TARGET_APP_ID]], 'keycloak' => [ 'clientId' => ['target' => self::TARGET_APP_ID], 'endpoint' => ['target' => self::TARGET_SECRET, 'key' => 'keycloakDomain'], diff --git a/tests/Migration/Unit/Resources/OAuth2ProviderTest.php b/tests/Migration/Unit/Resources/OAuth2ProviderTest.php index c24a06cc..c74b009b 100644 --- a/tests/Migration/Unit/Resources/OAuth2ProviderTest.php +++ b/tests/Migration/Unit/Resources/OAuth2ProviderTest.php @@ -25,6 +25,24 @@ public function testFromArrayAppwrite(): void $this->assertTrue($provider->isConfigured()); } + public function testFromArrayHuggingFace(): void + { + $provider = OAuth2Provider::fromArray('huggingface', [ + 'id' => 'huggingface', + 'enabled' => true, + 'clientId' => 'client-123', + 'clientSecret' => 'super-secret', + ]); + + $this->assertNotNull($provider); + $this->assertEquals('huggingface', $provider->getProviderKey()); + $this->assertTrue($provider->getEnabled()); + $this->assertEquals(['clientId' => 'client-123'], $provider->getSettings()); + $this->assertEquals('client-123', $provider->getDestinationAppId()); + $this->assertEquals([], $provider->getDestinationSecretFields()); + $this->assertTrue($provider->isConfigured()); + } + public function testFromArrayNeverCopiesSecrets(): void { foreach (\array_keys(OAuth2Provider::PROVIDERS) as $providerKey) { From 0144e335ae44114077e3b9966d13997835165d33 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 16:06:41 +1200 Subject: [PATCH 05/14] chore: pin utopia-php/database to Appwrite's feat-query-lib SHA Appwrite #11649 locks database at 5719edd. Staying on e593b78 would only prove the schema VO calls against an older query-lib surface. --- composer.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/composer.lock b/composer.lock index 1ca9e57b..d4cae1bf 100644 --- a/composer.lock +++ b/composer.lock @@ -248,23 +248,23 @@ }, { "name": "google/protobuf", - "version": "v5.35.1", + "version": "v5.36.0", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "55bb4a7d6739b5af0927b96213c1371a3afb7cfb" + "reference": "9c105104b54709ecd902494ab340ed2122789b2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/55bb4a7d6739b5af0927b96213c1371a3afb7cfb", - "reference": "55bb4a7d6739b5af0927b96213c1371a3afb7cfb", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/9c105104b54709ecd902494ab340ed2122789b2d", + "reference": "9c105104b54709ecd902494ab340ed2122789b2d", "shasum": "" }, "require": { "php": ">=8.2.0" }, "require-dev": { - "phpunit/phpunit": ">=11.5.0 <12.0.0" + "phpunit/phpunit": ">=11.5.50 <12.0.0" }, "suggest": { "ext-bcmath": "Need to support JSON deserialization" @@ -286,9 +286,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v5.35.1" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v5.36.0" }, - "time": "2026-06-11T21:19:23+00:00" + "time": "2026-08-20T13:06:50+00:00" }, { "name": "halaxa/json-machine", @@ -2612,12 +2612,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "e593b78c587e1f1d66b644979f5c4ff11dffcd31" + "reference": "5719eddbd77bb6b9a9916aec64ee3a8b0bc45c0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/e593b78c587e1f1d66b644979f5c4ff11dffcd31", - "reference": "e593b78c587e1f1d66b644979f5c4ff11dffcd31", + "url": "https://api.github.com/repos/utopia-php/database/zipball/5719eddbd77bb6b9a9916aec64ee3a8b0bc45c0f", + "reference": "5719eddbd77bb6b9a9916aec64ee3a8b0bc45c0f", "shasum": "" }, "require": { @@ -2632,7 +2632,7 @@ "utopia-php/mongo": "1.*", "utopia-php/pools": "2.*", "utopia-php/query": "0.4.*", - "utopia-php/validators": "0.4.*" + "utopia-php/validators": "0.4.* || 0.5.*" }, "require-dev": { "brianium/paratest": "7.20.*", @@ -2702,7 +2702,7 @@ "source": "https://github.com/utopia-php/database/tree/feat-query-lib", "issues": "https://github.com/utopia-php/database/issues" }, - "time": "2026-08-14T02:54:13+00:00" + "time": "2026-08-20T11:46:41+00:00" }, { "name": "utopia-php/dsn", From a11db206fd09bd047f36496226ab95ecbf30fbad Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 16:16:04 +1200 Subject: [PATCH 06/14] test: stub Swoole Coroutine when the extension is missing utopia-php/database feat-query-lib keys silenced events with Coroutine::getCid(). The CI image is vanilla PHP, so Memory-adapter tests fatalled before they could run. --- phpunit.xml | 2 +- tests/bootstrap.php | 7 +++++++ tests/stubs/SwooleCoroutine.php | 11 +++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 tests/bootstrap.php create mode 100644 tests/stubs/SwooleCoroutine.php diff --git a/phpunit.xml b/phpunit.xml index fad0deca..ce3ac877 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -2,7 +2,7 @@ diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..17c58c63 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,7 @@ + Date: Fri, 21 Aug 2026 16:16:04 +1200 Subject: [PATCH 07/14] fix: mark databases failed when metadata reload misses createDocument can persist a row whose subsequent getDocument is empty. That throw sat outside the failed-status handler, so a later skip could flip the unusable database to ready without a backing collection. --- src/Migration/Destinations/Appwrite.php | 13 +++--- .../AppwriteDatabaseStatusTest.php | 43 ++++++++++++++++++- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 75fb2219..43c273a5 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -788,15 +788,15 @@ protected function createDatabase(Database $resource): bool } $database = $this->dbForProject->createDocument(self::META_DATABASES, new UtopiaDocument($document)); - $database = $this->dbForProject->getDocument(self::META_DATABASES, $database->getId()); - if ($database->isEmpty()) { - throw new DatabaseException('Failed to reload created database '.$resource->getId()); - } + try { + $database = $this->dbForProject->getDocument(self::META_DATABASES, $database->getId()); - $resource->setSequence($database->getSequence()); + if ($database->isEmpty()) { + throw new DatabaseException('Failed to reload created database '.$resource->getId()); + } - try { + $resource->setSequence($database->getSequence()); $structure = $this->collectionStructureFor($resource); $this->dbForProject->createCollection( @@ -805,7 +805,6 @@ protected function createDatabase(Database $resource): bool $this->schemaIndexes($structure['indexes'] ?? []) ); } catch (\Throwable $e) { - // The metadata document exists but the database isn't usable; mark it failed before propagating. $this->markDatabaseFailed($resource->getId()); throw $e; } diff --git a/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php b/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php index bde31198..071cd591 100644 --- a/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php +++ b/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php @@ -34,6 +34,24 @@ public function run( } } +final class ReloadFailingProjectDatabase extends UtopiaDatabase +{ + public bool $failNextDatabasesRead = false; + + #[Override] + public function getDocument(string $collection, string $id, array $queries = [], bool $forUpdate = false): UtopiaDocument + { + $document = parent::getDocument($collection, $id, $queries, $forUpdate); + if ($this->failNextDatabasesRead && $collection === 'databases' && !$document->isEmpty()) { + $this->failNextDatabasesRead = false; + + return new UtopiaDocument(); + } + + return $document; + } +} + final class AppwriteDatabaseStatusTest extends TestCase { public function testDatabaseCreationOmitsStatusThroughLegacyAndExplicitEntrypoints(): void @@ -74,9 +92,30 @@ public function testDatabaseCreationPreservesLifecycleThroughLegacyAndExplicitEn } } - private function createProjectDatabase(bool $withStatus): UtopiaDatabase + public function testReloadFailureMarksTheDatabaseFailed(): void + { + $database = new ReloadFailingProjectDatabase( + new MemoryAdapter(), + new Cache(new MemoryCache()), + ); + $this->createProjectDatabase(withStatus: true, database: $database); + $database->failNextDatabasesRead = true; + + $destination = $this->runDatabaseTransfer($database, explicit: false); + + $created = $this->getDatabaseDocument($database); + $this->assertNotSame([], $this->errorMessages($destination)); + $this->assertStringContainsString('Failed to reload created database', $this->errorMessages($destination)[0]); + $this->assertSame('failed', $created->getAttribute('status')); + $this->assertTrue( + $database->getCollection('database_'.$created->getSequence())->isEmpty(), + 'A reload failure must not leave a backing collection behind the metadata document', + ); + } + + private function createProjectDatabase(bool $withStatus, ?UtopiaDatabase $database = null): UtopiaDatabase { - $database = new UtopiaDatabase( + $database ??= new UtopiaDatabase( new MemoryAdapter(), new Cache(new MemoryCache()), ); From b67ace1a95907ed8dd2ec2ef8215de145b63cb36 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 17:11:39 +1200 Subject: [PATCH 08/14] (chore): use caret ranges for Utopia dependencies Asterisk wildcards on utopia-php packages are replaced with equivalent caret constraints so Composer ranges stay consistent. --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 78ad8ffb..c2edd013 100644 --- a/composer.json +++ b/composer.json @@ -34,8 +34,8 @@ "ext-openssl": "*", "appwrite/appwrite": "^27.0", "utopia-php/database": "dev-feat-query-lib as 7.0.0", - "utopia-php/storage": "4.*", - "utopia-php/dsn": "0.2.*", + "utopia-php/storage": "^4.0", + "utopia-php/dsn": "^0.2", "halaxa/json-machine": "^1.2" }, "require-dev": { From 7a3a60e4f5516ba860f1e970a09b0582f66b51b4 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 17:23:34 +1200 Subject: [PATCH 09/14] (chore): refresh lockfile after caret Utopia constraints Keep composer.json and composer.lock in sync so `composer validate` passes, and pin utopia-php/database to the current query-lib HEAD. --- composer.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/composer.lock b/composer.lock index d4cae1bf..591fcbb1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e20f678bfb397f89a6cfa0083988f539", + "content-hash": "669e0b5ccd20bfaf8cacb8461b605d49", "packages": [ { "name": "adhocore/jwt", @@ -2612,12 +2612,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "5719eddbd77bb6b9a9916aec64ee3a8b0bc45c0f" + "reference": "798f294c1615102c92f8e8f7f8b3d18f36ad083b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/5719eddbd77bb6b9a9916aec64ee3a8b0bc45c0f", - "reference": "5719eddbd77bb6b9a9916aec64ee3a8b0bc45c0f", + "url": "https://api.github.com/repos/utopia-php/database/zipball/798f294c1615102c92f8e8f7f8b3d18f36ad083b", + "reference": "798f294c1615102c92f8e8f7f8b3d18f36ad083b", "shasum": "" }, "require": { @@ -2626,13 +2626,13 @@ "ext-pdo": "*", "ext-redis": "*", "php": ">=8.5", - "utopia-php/async": "0.1.*", - "utopia-php/cache": "4.*", - "utopia-php/console": "0.1.*", - "utopia-php/mongo": "1.*", - "utopia-php/pools": "2.*", - "utopia-php/query": "0.4.*", - "utopia-php/validators": "0.4.* || 0.5.*" + "utopia-php/async": "^0.1", + "utopia-php/cache": "^4.0", + "utopia-php/console": "^0.1", + "utopia-php/mongo": "^1.0", + "utopia-php/pools": "^2.0", + "utopia-php/query": "^0.4", + "utopia-php/validators": "^0.4 || ^0.5" }, "require-dev": { "brianium/paratest": "7.20.*", @@ -2642,7 +2642,7 @@ "phpunit/phpunit": "12.5.*", "rregeer/phpunit-coverage-check": "0.3.*", "swoole/ide-helper": "5.1.3", - "utopia-php/cli": "0.22.*" + "utopia-php/cli": "^0.22" }, "suggest": { "ext-pdo": "Needed to support MariaDB, MySQL or SQLite Database Adapter", @@ -2702,7 +2702,7 @@ "source": "https://github.com/utopia-php/database/tree/feat-query-lib", "issues": "https://github.com/utopia-php/database/issues" }, - "time": "2026-08-20T11:46:41+00:00" + "time": "2026-08-21T05:22:01+00:00" }, { "name": "utopia-php/dsn", From f2de906c9a84b9c911658d446434ccc9acce7c24 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 18:52:50 +1200 Subject: [PATCH 10/14] (feat): pass Collection to createCollection Database::createCollection no longer accepts a string id. --- composer.lock | 8 ++++---- src/Migration/Destinations/Appwrite.php | 19 ++++--------------- .../AppwriteDatabaseStatusTest.php | 3 ++- .../Destinations/AppwriteIndexLengthsTest.php | 13 +++++++------ 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/composer.lock b/composer.lock index 591fcbb1..fabd1850 100644 --- a/composer.lock +++ b/composer.lock @@ -2612,12 +2612,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "798f294c1615102c92f8e8f7f8b3d18f36ad083b" + "reference": "808f90bff1a7ccec78cc2eabebd30c2454a9c896" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/798f294c1615102c92f8e8f7f8b3d18f36ad083b", - "reference": "798f294c1615102c92f8e8f7f8b3d18f36ad083b", + "url": "https://api.github.com/repos/utopia-php/database/zipball/808f90bff1a7ccec78cc2eabebd30c2454a9c896", + "reference": "808f90bff1a7ccec78cc2eabebd30c2454a9c896", "shasum": "" }, "require": { @@ -2702,7 +2702,7 @@ "source": "https://github.com/utopia-php/database/tree/feat-query-lib", "issues": "https://github.com/utopia-php/database/issues" }, - "time": "2026-08-21T05:22:01+00:00" + "time": "2026-08-21T06:50:33+00:00" }, { "name": "utopia-php/dsn", diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 43c273a5..c23ea0dc 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -27,6 +27,7 @@ use Utopia\Database\Adapter\Feature\Spatial; use Utopia\Database\Attribute as UtopiaAttribute; use Utopia\Database\Capability; +use Utopia\Database\Collection; use Utopia\Database\Database as UtopiaDatabase; use Utopia\Database\DateTime; use Utopia\Database\Document as UtopiaDocument; @@ -743,11 +744,7 @@ protected function createDatabase(Database $resource): bool try { $structure = $this->collectionStructureFor($resource); - $this->dbForProject->createCollection( - $this->databaseCollectionId($existing), - $this->schemaAttributes($structure['attributes'] ?? []), - $this->schemaIndexes($structure['indexes'] ?? []) - ); + $this->dbForProject->createCollection(new Collection(id: $this->databaseCollectionId($existing), attributes: $this->schemaAttributes($structure['attributes'] ?? []), indexes: $this->schemaIndexes($structure['indexes'] ?? []))); } catch (\Throwable $e) { $this->markDatabaseFailed($resource->getId()); throw $e; @@ -799,11 +796,7 @@ protected function createDatabase(Database $resource): bool $resource->setSequence($database->getSequence()); $structure = $this->collectionStructureFor($resource); - $this->dbForProject->createCollection( - $this->databaseCollectionId($database), - $this->schemaAttributes($structure['attributes'] ?? []), - $this->schemaIndexes($structure['indexes'] ?? []) - ); + $this->dbForProject->createCollection(new Collection(id: $this->databaseCollectionId($database), attributes: $this->schemaAttributes($structure['attributes'] ?? []), indexes: $this->schemaIndexes($structure['indexes'] ?? []))); } catch (\Throwable $e) { $this->markDatabaseFailed($resource->getId()); throw $e; @@ -927,11 +920,7 @@ protected function createEntity(Table $resource): bool $resource->setSequence($table->getSequence()); - $dbForDatabases->createCollection( - $this->tableCollectionId($database, $table), - permissions: $resource->getPermissions(), - documentSecurity: $resource->getRowSecurity() - ); + $dbForDatabases->createCollection(new Collection(id: $this->tableCollectionId($database, $table), permissions: $resource->getPermissions(), documentSecurity: $resource->getRowSecurity())); return true; } diff --git a/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php b/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php index 071cd591..2081838c 100644 --- a/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php +++ b/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php @@ -8,6 +8,7 @@ use Utopia\Cache\Cache; use Utopia\Database\Adapter\Memory as MemoryAdapter; use Utopia\Database\Attribute as UtopiaAttribute; +use Utopia\Database\Collection; use Utopia\Database\Database as UtopiaDatabase; use Utopia\Database\Document as UtopiaDocument; use Utopia\Migration\Destinations\Appwrite as AppwriteDestination; @@ -137,7 +138,7 @@ private function createProjectDatabase(bool $withStatus, ?UtopiaDatabase $databa $attributes[] = $this->attribute('status', ColumnType::String, size: 16); } - $database->createCollection('databases', $attributes); + $database->createCollection(new Collection(id: 'databases', attributes: $attributes)); return $database; } diff --git a/tests/Migration/Unit/Destinations/AppwriteIndexLengthsTest.php b/tests/Migration/Unit/Destinations/AppwriteIndexLengthsTest.php index e7a70388..084ed576 100644 --- a/tests/Migration/Unit/Destinations/AppwriteIndexLengthsTest.php +++ b/tests/Migration/Unit/Destinations/AppwriteIndexLengthsTest.php @@ -9,6 +9,7 @@ use Utopia\Cache\Cache; use Utopia\Database\Adapter\Memory as MemoryAdapter; use Utopia\Database\Attribute as UtopiaAttribute; +use Utopia\Database\Collection; use Utopia\Database\Database as UtopiaDatabase; use Utopia\Database\Document as UtopiaDocument; use Utopia\Migration\Destinations\Appwrite as AppwriteDestination; @@ -246,16 +247,16 @@ private function projectDatabase(): UtopiaDatabase ->setNamespace('_project'); $database->create(); - $database->createCollection('databases', [ + $database->createCollection(new Collection(id: 'databases', attributes: [ $this->attribute('name', ColumnType::String, required: true, size: 256), $this->attribute('enabled', ColumnType::Boolean, default: true), $this->attribute('search', ColumnType::String, size: 16384), $this->attribute('originalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), $this->attribute('type', ColumnType::String, default: 'tablesdb', size: 128), $this->attribute('database', ColumnType::String, size: 2000), - ]); + ])); - $database->createCollection('attributes', [ + $database->createCollection(new Collection(id: 'attributes', attributes: [ $this->attribute('key', ColumnType::String, size: 256), $this->attribute('databaseInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), $this->attribute('databaseId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), @@ -273,9 +274,9 @@ private function projectDatabase(): UtopiaDatabase $this->attribute('filters', ColumnType::String, size: 64, array: true), $this->attribute('options', ColumnType::String, size: 16384, filters: ['json']), $this->attribute('error', ColumnType::String, size: 2048), - ]); + ])); - $database->createCollection('indexes', [ + $database->createCollection(new Collection(id: 'indexes', attributes: [ $this->attribute('key', ColumnType::String, size: 256), $this->attribute('status', ColumnType::String, size: 64), $this->attribute('databaseInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), @@ -287,7 +288,7 @@ private function projectDatabase(): UtopiaDatabase $this->attribute('lengths', ColumnType::Integer, array: true), $this->attribute('orders', ColumnType::String, size: 4, array: true), $this->attribute('error', ColumnType::String, size: 2048), - ]); + ])); return $database; } From 3fccb0c29d824410c9803936d0d8eae38cb49ee8 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 22:24:19 +1200 Subject: [PATCH 11/14] (fix): pass Attribute models to checkAttribute Database::checkAttribute now requires Attribute. Build schema models from the resource key so metadata document IDs are not used as attribute keys. --- src/Migration/Destinations/Appwrite.php | 18 ++++- .../AppwriteCheckAttributeTest.php | 66 +++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 tests/Migration/Unit/Destinations/AppwriteCheckAttributeTest.php diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index c23ea0dc..23e91dce 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -1104,7 +1104,19 @@ protected function createField(Column|Attribute $resource): bool '$updatedAt' => $updatedAt, ]); - $this->dbForProject->checkAttribute($table, $column); + $this->dbForProject->checkAttribute($table, UtopiaAttribute::fromArray([ + 'key' => $resource->getKey(), + 'type' => $type, + 'size' => $resource->getSize(), + 'required' => $resource->isRequired(), + 'signed' => $resource->isSigned(), + 'default' => $resource->getDefault(), + 'array' => $resource->isArray(), + 'format' => $resource->getFormat() !== '' ? $resource->getFormat() : null, + 'formatOptions' => $resource->getFormatOptions(), + 'filters' => $resource->getFilters(), + 'options' => $resource->getOptions() !== [] ? $resource->getOptions() : null, + ])); $column = $this->dbForProject->createDocument(self::META_ATTRIBUTES, $column); } catch (DuplicateException $e) { @@ -1318,7 +1330,7 @@ private function schemaAttributes(array $attributes): array return $attr; } if ($attr instanceof UtopiaDocument) { - return UtopiaAttribute::fromDocument($attr); + return UtopiaAttribute::fromArray($attr->getArrayCopy()); } return UtopiaAttribute::fromArray(\is_array($attr) ? $attr : []); @@ -1336,7 +1348,7 @@ private function schemaIndexes(array $indexes): array return $index; } if ($index instanceof UtopiaDocument) { - return UtopiaIndex::fromDocument($index); + return UtopiaIndex::fromArray($index->getArrayCopy()); } return UtopiaIndex::fromArray(\is_array($index) ? $index : []); diff --git a/tests/Migration/Unit/Destinations/AppwriteCheckAttributeTest.php b/tests/Migration/Unit/Destinations/AppwriteCheckAttributeTest.php new file mode 100644 index 00000000..7825f7e9 --- /dev/null +++ b/tests/Migration/Unit/Destinations/AppwriteCheckAttributeTest.php @@ -0,0 +1,66 @@ +assertStringContainsString( + "checkAttribute(\$table, UtopiaAttribute::fromArray([", + $source, + ); + $this->assertStringContainsString( + "'key' => \$resource->getKey(),", + $source, + ); + $this->assertStringNotContainsString( + 'checkAttribute($table, $column)', + $source, + ); + + $column = new UtopiaDocument([ + '$id' => '1_2_title', + 'key' => 'title', + 'type' => 'string', + 'size' => 128, + 'required' => true, + 'signed' => true, + 'default' => null, + 'array' => false, + 'format' => '', + 'formatOptions' => [], + 'filters' => [], + 'options' => [], + ]); + + $fromDocumentCopy = UtopiaAttribute::fromArray($column->getArrayCopy()); + $this->assertSame('1_2_title', $fromDocumentCopy->key); + + $attribute = UtopiaAttribute::fromArray([ + 'key' => 'title', + 'type' => 'string', + 'size' => 128, + 'required' => true, + 'signed' => true, + 'default' => null, + 'array' => false, + 'format' => null, + 'formatOptions' => [], + 'filters' => [], + 'options' => null, + ]); + + $this->assertInstanceOf(UtopiaAttribute::class, $attribute); + $this->assertSame('title', $attribute->key); + $this->assertSame(ColumnType::String, $attribute->type); + $this->assertSame(128, $attribute->size); + } +} From 3a5fb77ac4ab5f93a09fd1c8537434e11a3d16d0 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 22:24:34 +1200 Subject: [PATCH 12/14] (fix): pass Attribute models into checkAttribute Appwrite E2E migrations failed because checkAttribute now requires Attribute, and the destination still handed it a metadata Document. --- composer.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/composer.lock b/composer.lock index fabd1850..3f7ce063 100644 --- a/composer.lock +++ b/composer.lock @@ -2612,12 +2612,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "808f90bff1a7ccec78cc2eabebd30c2454a9c896" + "reference": "196db02d6e2eadb4d42c192d5a0f260c06b3e1db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/808f90bff1a7ccec78cc2eabebd30c2454a9c896", - "reference": "808f90bff1a7ccec78cc2eabebd30c2454a9c896", + "url": "https://api.github.com/repos/utopia-php/database/zipball/196db02d6e2eadb4d42c192d5a0f260c06b3e1db", + "reference": "196db02d6e2eadb4d42c192d5a0f260c06b3e1db", "shasum": "" }, "require": { @@ -2631,7 +2631,7 @@ "utopia-php/console": "^0.1", "utopia-php/mongo": "^1.0", "utopia-php/pools": "^2.0", - "utopia-php/query": "^0.4", + "utopia-php/query": "^0.5", "utopia-php/validators": "^0.4 || ^0.5" }, "require-dev": { @@ -2702,7 +2702,7 @@ "source": "https://github.com/utopia-php/database/tree/feat-query-lib", "issues": "https://github.com/utopia-php/database/issues" }, - "time": "2026-08-21T06:50:33+00:00" + "time": "2026-08-21T09:27:18+00:00" }, { "name": "utopia-php/dsn", @@ -2916,16 +2916,16 @@ }, { "name": "utopia-php/query", - "version": "0.4.0", + "version": "0.5.0", "source": { "type": "git", "url": "https://github.com/utopia-php/query.git", - "reference": "c334515035a2ab0aa49176eeca96de3e1c096e22" + "reference": "802821c6fb0470410e1f0e65561cc16224c343c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/query/zipball/c334515035a2ab0aa49176eeca96de3e1c096e22", - "reference": "c334515035a2ab0aa49176eeca96de3e1c096e22", + "url": "https://api.github.com/repos/utopia-php/query/zipball/802821c6fb0470410e1f0e65561cc16224c343c0", + "reference": "802821c6fb0470410e1f0e65561cc16224c343c0", "shasum": "" }, "require": { @@ -2959,9 +2959,9 @@ ], "support": { "issues": "https://github.com/utopia-php/query/issues", - "source": "https://github.com/utopia-php/query/tree/0.4.0" + "source": "https://github.com/utopia-php/query/tree/0.5.0" }, - "time": "2026-08-14T01:42:36+00:00" + "time": "2026-08-21T06:16:10+00:00" }, { "name": "utopia-php/span", From 18c09d40a53baa31e59abf9b52698c78c0f9b731 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 22:36:21 +1200 Subject: [PATCH 13/14] (fix): recover failed databases on OnDuplicate::Fail retries A reload failure leaves a metadata document in `failed` with no backing collection. Recovery only ran when onDuplicate was not Fail, so the default policy retried createDocument against the existing ID and stranded the database. --- src/Migration/Destinations/Appwrite.php | 16 +++++----- .../AppwriteDatabaseStatusTest.php | 29 +++++++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 23e91dce..3a2dc781 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -682,22 +682,22 @@ protected function createDatabase(Database $resource): bool $createdAt = $this->normalizeDateTime($resource->getCreatedAt()); $updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt); - if ($this->onDuplicate !== OnDuplicate::Fail) { - $existing = $this->dbForProject->getDocument(self::META_DATABASES, $resource->getId()); + $existing = $this->dbForProject->getDocument(self::META_DATABASES, $resource->getId()); + $isFailed = ! $existing->isEmpty() + && $this->getSupportForDatabaseStatus() + && $existing->getAttribute('status') === self::DATABASE_STATUS_FAILED; + + if ($this->onDuplicate !== OnDuplicate::Fail || $isFailed) { $action = $this->onDuplicate->resolveSchemaAction( !$existing->isEmpty(), $updatedAt, $existing->getUpdatedAt(), ); - $isFailed = ! $existing->isEmpty() - && $this->getSupportForDatabaseStatus() - && $existing->getAttribute('status') === self::DATABASE_STATUS_FAILED; - if ($isFailed) { // A prior run created the metadata document but left the database unusable (its backing - // collection may be missing). Force Overwrite — regardless of timestamps or spec match — - // so the recovery path recreates the collection instead of skipping it forever. + // collection may be missing). Force Overwrite — regardless of timestamps, spec match, or + // OnDuplicate::Fail — so retries recreate the collection instead of hitting the existing ID. $action = SchemaAction::Overwrite; } elseif ($action !== SchemaAction::Create && $this->databaseSpecMatches($existing, $resource)) { // Spec match → skip work. Create excluded; nothing on dest to match against. diff --git a/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php b/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php index 2081838c..05ee2ae9 100644 --- a/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php +++ b/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php @@ -114,6 +114,35 @@ public function testReloadFailureMarksTheDatabaseFailed(): void ); } + public function testFailedDatabaseRetrySucceedsUnderOnDuplicateFail(): void + { + $database = new ReloadFailingProjectDatabase( + new MemoryAdapter(), + new Cache(new MemoryCache()), + ); + $this->createProjectDatabase(withStatus: true, database: $database); + $database->failNextDatabasesRead = true; + + $this->runDatabaseTransfer($database, explicit: false); + + $failed = $this->getDatabaseDocument($database); + $this->assertSame('failed', $failed->getAttribute('status')); + $this->assertTrue( + $database->getCollection('database_'.$failed->getSequence())->isEmpty(), + ); + + $destination = $this->runDatabaseTransfer($database, explicit: false); + + $recovered = $this->getDatabaseDocument($database); + $this->assertSame([], $this->errorMessages($destination)); + $this->assertSame('ready', $recovered->getAttribute('status')); + $this->assertSame($failed->getSequence(), $recovered->getSequence()); + $this->assertFalse( + $database->getCollection('database_'.$recovered->getSequence())->isEmpty(), + 'A Fail retry must recreate the backing collection for a previously failed database', + ); + } + private function createProjectDatabase(bool $withStatus, ?UtopiaDatabase $database = null): UtopiaDatabase { $database ??= new UtopiaDatabase( From 04ab893e1a9ab69967ec8d7ab7a4eb4e699ee1ce Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 22:53:45 +1200 Subject: [PATCH 14/14] (refactor): use Schema\Order and wrap Collection constructors Index types already used IndexType; column direction was still a raw ASC string. Collection constructors with multiple named params were also jammed on one line. --- bin/MigrationCLI.php | 7 +- composer.json | 4 + composer.lock | 59 +++++++++--- src/Migration/Destinations/Appwrite.php | 18 +++- .../AppwriteDatabaseStatusTest.php | 5 +- .../Destinations/AppwriteIndexLengthsTest.php | 96 ++++++++++--------- 6 files changed, 125 insertions(+), 64 deletions(-) diff --git a/bin/MigrationCLI.php b/bin/MigrationCLI.php index e6681148..3d472faf 100644 --- a/bin/MigrationCLI.php +++ b/bin/MigrationCLI.php @@ -21,6 +21,7 @@ use Utopia\Migration\Transfer; use Utopia\Query\Schema\ColumnType; use Utopia\Query\Schema\IndexType; +use Utopia\Query\Schema\Order; /** * Migrations CLI Tool @@ -134,21 +135,21 @@ class MigrationCLI 'type' => IndexType::Key->value, 'attributes' => ['name'], 'lengths' => [Database::LENGTH_KEY], - 'orders' => ['ASC'], + 'orders' => [Order::Asc->value], ], [ '$id' => '_key_enabled', 'type' => IndexType::Key->value, 'attributes' => ['enabled'], 'lengths' => [], - 'orders' => ['ASC'], + 'orders' => [Order::Asc->value], ], [ '$id' => '_key_documentSecurity', 'type' => IndexType::Key->value, 'attributes' => ['documentSecurity'], 'lengths' => [], - 'orders' => ['ASC'], + 'orders' => [Order::Asc->value], ], ], ]; diff --git a/composer.json b/composer.json index c2edd013..27ef7b47 100644 --- a/composer.json +++ b/composer.json @@ -50,6 +50,10 @@ "type": "vcs", "url": "https://github.com/utopia-php/database.git" }, + { + "type": "vcs", + "url": "https://github.com/utopia-php/query.git" + }, { "type": "vcs", "url": "https://github.com/utopia-php/async.git" diff --git a/composer.lock b/composer.lock index 3f7ce063..780b84b5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "669e0b5ccd20bfaf8cacb8461b605d49", + "content-hash": "2004ea1027b967193c5a9247cff21c35", "packages": [ { "name": "adhocore/jwt", @@ -2612,12 +2612,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "196db02d6e2eadb4d42c192d5a0f260c06b3e1db" + "reference": "32823ded323e35c4bd5029cf904cc8b269b62b06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/196db02d6e2eadb4d42c192d5a0f260c06b3e1db", - "reference": "196db02d6e2eadb4d42c192d5a0f260c06b3e1db", + "url": "https://api.github.com/repos/utopia-php/database/zipball/32823ded323e35c4bd5029cf904cc8b269b62b06", + "reference": "32823ded323e35c4bd5029cf904cc8b269b62b06", "shasum": "" }, "require": { @@ -2631,7 +2631,7 @@ "utopia-php/console": "^0.1", "utopia-php/mongo": "^1.0", "utopia-php/pools": "^2.0", - "utopia-php/query": "^0.5", + "utopia-php/query": "dev-feat-schema-order as 0.5.0", "utopia-php/validators": "^0.4 || ^0.5" }, "require-dev": { @@ -2702,7 +2702,7 @@ "source": "https://github.com/utopia-php/database/tree/feat-query-lib", "issues": "https://github.com/utopia-php/database/issues" }, - "time": "2026-08-21T09:27:18+00:00" + "time": "2026-08-21T10:48:05+00:00" }, { "name": "utopia-php/dsn", @@ -2916,16 +2916,16 @@ }, { "name": "utopia-php/query", - "version": "0.5.0", + "version": "dev-feat-schema-order", "source": { "type": "git", "url": "https://github.com/utopia-php/query.git", - "reference": "802821c6fb0470410e1f0e65561cc16224c343c0" + "reference": "08b8e70214ae2d9315762afc1a6135c6864f7f7c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/query/zipball/802821c6fb0470410e1f0e65561cc16224c343c0", - "reference": "802821c6fb0470410e1f0e65561cc16224c343c0", + "url": "https://api.github.com/repos/utopia-php/query/zipball/08b8e70214ae2d9315762afc1a6135c6864f7f7c", + "reference": "08b8e70214ae2d9315762afc1a6135c6864f7f7c", "shasum": "" }, "require": { @@ -2945,7 +2945,38 @@ "Utopia\\Query\\": "src/Query" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "Tests\\Query\\": "tests/Query", + "Tests\\Integration\\": "tests/Integration" + } + }, + "scripts": { + "test": [ + "vendor/bin/paratest --testsuite Query --processes=auto --exclude-group=performance" + ], + "test:coverage": [ + "vendor/bin/paratest --testsuite Query --processes=auto --exclude-group=performance --coverage-php coverage/unit.cov" + ], + "test:performance": [ + "vendor/bin/phpunit --testsuite Query --group=performance" + ], + "test:integration": [ + "vendor/bin/phpunit --testsuite Integration" + ], + "test:integration:coverage": [ + "vendor/bin/phpunit --testsuite Integration --coverage-php coverage/integration.cov" + ], + "lint": [ + "php -d memory_limit=2G ./vendor/bin/pint --test" + ], + "format": [ + "php -d memory_limit=2G ./vendor/bin/pint" + ], + "check": [ + "./vendor/bin/phpstan analyse --level max src tests --memory-limit 2G" + ] + }, "license": [ "MIT" ], @@ -2958,10 +2989,10 @@ "utopia" ], "support": { - "issues": "https://github.com/utopia-php/query/issues", - "source": "https://github.com/utopia-php/query/tree/0.5.0" + "source": "https://github.com/utopia-php/query/tree/feat-schema-order", + "issues": "https://github.com/utopia-php/query/issues" }, - "time": "2026-08-21T06:16:10+00:00" + "time": "2026-08-21T10:44:38+00:00" }, { "name": "utopia-php/span", diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 3a2dc781..ce082875 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -744,7 +744,11 @@ protected function createDatabase(Database $resource): bool try { $structure = $this->collectionStructureFor($resource); - $this->dbForProject->createCollection(new Collection(id: $this->databaseCollectionId($existing), attributes: $this->schemaAttributes($structure['attributes'] ?? []), indexes: $this->schemaIndexes($structure['indexes'] ?? []))); + $this->dbForProject->createCollection(new Collection( + id: $this->databaseCollectionId($existing), + attributes: $this->schemaAttributes($structure['attributes'] ?? []), + indexes: $this->schemaIndexes($structure['indexes'] ?? []), + )); } catch (\Throwable $e) { $this->markDatabaseFailed($resource->getId()); throw $e; @@ -796,7 +800,11 @@ protected function createDatabase(Database $resource): bool $resource->setSequence($database->getSequence()); $structure = $this->collectionStructureFor($resource); - $this->dbForProject->createCollection(new Collection(id: $this->databaseCollectionId($database), attributes: $this->schemaAttributes($structure['attributes'] ?? []), indexes: $this->schemaIndexes($structure['indexes'] ?? []))); + $this->dbForProject->createCollection(new Collection( + id: $this->databaseCollectionId($database), + attributes: $this->schemaAttributes($structure['attributes'] ?? []), + indexes: $this->schemaIndexes($structure['indexes'] ?? []), + )); } catch (\Throwable $e) { $this->markDatabaseFailed($resource->getId()); throw $e; @@ -920,7 +928,11 @@ protected function createEntity(Table $resource): bool $resource->setSequence($table->getSequence()); - $dbForDatabases->createCollection(new Collection(id: $this->tableCollectionId($database, $table), permissions: $resource->getPermissions(), documentSecurity: $resource->getRowSecurity())); + $dbForDatabases->createCollection(new Collection( + id: $this->tableCollectionId($database, $table), + permissions: $resource->getPermissions(), + documentSecurity: $resource->getRowSecurity(), + )); return true; } diff --git a/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php b/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php index 05ee2ae9..40a56178 100644 --- a/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php +++ b/tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php @@ -167,7 +167,10 @@ private function createProjectDatabase(bool $withStatus, ?UtopiaDatabase $databa $attributes[] = $this->attribute('status', ColumnType::String, size: 16); } - $database->createCollection(new Collection(id: 'databases', attributes: $attributes)); + $database->createCollection(new Collection( + id: 'databases', + attributes: $attributes, + )); return $database; } diff --git a/tests/Migration/Unit/Destinations/AppwriteIndexLengthsTest.php b/tests/Migration/Unit/Destinations/AppwriteIndexLengthsTest.php index 084ed576..2cb3dd1a 100644 --- a/tests/Migration/Unit/Destinations/AppwriteIndexLengthsTest.php +++ b/tests/Migration/Unit/Destinations/AppwriteIndexLengthsTest.php @@ -21,6 +21,7 @@ use Utopia\Migration\Resources\Database\Table; use Utopia\Migration\Transfer; use Utopia\Query\Schema\ColumnType; +use Utopia\Query\Schema\Order; use Utopia\Tests\Unit\Adapters\MockSource; /** @@ -178,7 +179,7 @@ private function transferIndex( type: 'key', columns: ['reference', 'channel'], lengths: $withLengths, - orders: ['ASC', 'ASC'], + orders: [Order::Asc->value, Order::Asc->value], createdAt: $updatedAt, updatedAt: $updatedAt, ); @@ -247,48 +248,57 @@ private function projectDatabase(): UtopiaDatabase ->setNamespace('_project'); $database->create(); - $database->createCollection(new Collection(id: 'databases', attributes: [ - $this->attribute('name', ColumnType::String, required: true, size: 256), - $this->attribute('enabled', ColumnType::Boolean, default: true), - $this->attribute('search', ColumnType::String, size: 16384), - $this->attribute('originalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('type', ColumnType::String, default: 'tablesdb', size: 128), - $this->attribute('database', ColumnType::String, size: 2000), - ])); - - $database->createCollection(new Collection(id: 'attributes', attributes: [ - $this->attribute('key', ColumnType::String, size: 256), - $this->attribute('databaseInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('databaseId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('collectionInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('collectionId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('type', ColumnType::String, size: 256), - $this->attribute('status', ColumnType::String, size: 64), - $this->attribute('size', ColumnType::Integer), - $this->attribute('required', ColumnType::Boolean, default: false), - $this->attribute('signed', ColumnType::Boolean, default: true), - $this->attribute('default', ColumnType::String, size: 16384), - $this->attribute('array', ColumnType::Boolean, default: false), - $this->attribute('format', ColumnType::String, size: 64), - $this->attribute('formatOptions', ColumnType::String, size: 16384, filters: ['json']), - $this->attribute('filters', ColumnType::String, size: 64, array: true), - $this->attribute('options', ColumnType::String, size: 16384, filters: ['json']), - $this->attribute('error', ColumnType::String, size: 2048), - ])); - - $database->createCollection(new Collection(id: 'indexes', attributes: [ - $this->attribute('key', ColumnType::String, size: 256), - $this->attribute('status', ColumnType::String, size: 64), - $this->attribute('databaseInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('databaseId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('collectionInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('collectionId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), - $this->attribute('type', ColumnType::String, size: 16), - $this->attribute('attributes', ColumnType::String, size: 256, array: true), - $this->attribute('lengths', ColumnType::Integer, array: true), - $this->attribute('orders', ColumnType::String, size: 4, array: true), - $this->attribute('error', ColumnType::String, size: 2048), - ])); + $database->createCollection(new Collection( + id: 'databases', + attributes: [ + $this->attribute('name', ColumnType::String, required: true, size: 256), + $this->attribute('enabled', ColumnType::Boolean, default: true), + $this->attribute('search', ColumnType::String, size: 16384), + $this->attribute('originalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('type', ColumnType::String, default: 'tablesdb', size: 128), + $this->attribute('database', ColumnType::String, size: 2000), + ], + )); + + $database->createCollection(new Collection( + id: 'attributes', + attributes: [ + $this->attribute('key', ColumnType::String, size: 256), + $this->attribute('databaseInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('databaseId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('collectionInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('collectionId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('type', ColumnType::String, size: 256), + $this->attribute('status', ColumnType::String, size: 64), + $this->attribute('size', ColumnType::Integer), + $this->attribute('required', ColumnType::Boolean, default: false), + $this->attribute('signed', ColumnType::Boolean, default: true), + $this->attribute('default', ColumnType::String, size: 16384), + $this->attribute('array', ColumnType::Boolean, default: false), + $this->attribute('format', ColumnType::String, size: 64), + $this->attribute('formatOptions', ColumnType::String, size: 16384, filters: ['json']), + $this->attribute('filters', ColumnType::String, size: 64, array: true), + $this->attribute('options', ColumnType::String, size: 16384, filters: ['json']), + $this->attribute('error', ColumnType::String, size: 2048), + ], + )); + + $database->createCollection(new Collection( + id: 'indexes', + attributes: [ + $this->attribute('key', ColumnType::String, size: 256), + $this->attribute('status', ColumnType::String, size: 64), + $this->attribute('databaseInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('databaseId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('collectionInternalId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('collectionId', ColumnType::String, size: UtopiaDatabase::LENGTH_KEY), + $this->attribute('type', ColumnType::String, size: 16), + $this->attribute('attributes', ColumnType::String, size: 256, array: true), + $this->attribute('lengths', ColumnType::Integer, array: true), + $this->attribute('orders', ColumnType::String, size: 4, array: true), + $this->attribute('error', ColumnType::String, size: 2048), + ], + )); return $database; }