From 93bd6b39a47cc3489e20e174b614e2585ebf4ad6 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 13 Nov 2025 16:47:00 +0530 Subject: [PATCH] updated migrations + index finding issue in vectordb --- composer.json | 4 +- composer.lock | 160 +++-- .../Platform/Modules/Databases/Constants.php | 2 +- .../Http/VectorDB/Collections/Create.php | 26 +- src/Appwrite/Platform/Workers/Migrations.php | 1 + .../Services/Migrations/MigrationsBase.php | 556 +++++++++++++++++- 6 files changed, 651 insertions(+), 98 deletions(-) diff --git a/composer.json b/composer.json index 910cbcb5e4..e16cd450b8 100644 --- a/composer.json +++ b/composer.json @@ -54,8 +54,8 @@ "utopia-php/cache": "0.13.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "dev-var_object as 1.4.5", - "utopia-php/agents": "dev-feat-embeddings", + "utopia-php/database": "3.*", + "utopia-php/agents": "0.*", "utopia-php/detector": "0.2.*", "utopia-php/domains": "0.9.*", "utopia-php/emails": "0.6.*", diff --git a/composer.lock b/composer.lock index 62cccc95f7..19724e39da 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": "f9b66abe46119ae12f0cf2becc8bc299", + "content-hash": "a2f0965b1d70d23bbfc5a948dafabd1e", "packages": [ { "name": "adhocore/jwt", @@ -161,16 +161,16 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.19.1", + "version": "0.19.2", "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "7bd0cc3cb97de625d7b07230bd91b121f88e72ae" + "reference": "e5c142519df5aced37de9c302971c29c079ce3d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/runtimes/zipball/7bd0cc3cb97de625d7b07230bd91b121f88e72ae", - "reference": "7bd0cc3cb97de625d7b07230bd91b121f88e72ae", + "url": "https://api.github.com/repos/appwrite/runtimes/zipball/e5c142519df5aced37de9c302971c29c079ce3d9", + "reference": "e5c142519df5aced37de9c302971c29c079ce3d9", "shasum": "" }, "require": { @@ -210,9 +210,9 @@ ], "support": { "issues": "https://github.com/appwrite/runtimes/issues", - "source": "https://github.com/appwrite/runtimes/tree/0.19.1" + "source": "https://github.com/appwrite/runtimes/tree/0.19.2" }, - "time": "2025-05-27T07:12:56+00:00" + "time": "2025-11-11T13:44:44+00:00" }, { "name": "beberlei/assert", @@ -754,16 +754,16 @@ }, { "name": "google/protobuf", - "version": "v4.33.0", + "version": "v4.33.1", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "b50269e23204e5ae859a326ec3d90f09efe3047d" + "reference": "0cd73ccf0cd26c3e72299cce1ea6144091a57e12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/b50269e23204e5ae859a326ec3d90f09efe3047d", - "reference": "b50269e23204e5ae859a326ec3d90f09efe3047d", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/0cd73ccf0cd26c3e72299cce1ea6144091a57e12", + "reference": "0cd73ccf0cd26c3e72299cce1ea6144091a57e12", "shasum": "" }, "require": { @@ -792,9 +792,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.0" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.1" }, - "time": "2025-10-15T20:10:28+00:00" + "time": "2025-11-12T21:58:05+00:00" }, { "name": "league/csv", @@ -2668,16 +2668,16 @@ }, { "name": "symfony/http-client", - "version": "v7.3.4", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62" + "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/4b62871a01c49457cf2a8e560af7ee8a94b87a62", - "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62", + "url": "https://api.github.com/repos/symfony/http-client/zipball/3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de", + "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de", "shasum": "" }, "require": { @@ -2744,7 +2744,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.4" + "source": "https://github.com/symfony/http-client/tree/v7.3.6" }, "funding": [ { @@ -2764,7 +2764,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-11-05T17:41:46+00:00" }, { "name": "symfony/http-client-contracts", @@ -3171,16 +3171,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -3234,7 +3234,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -3245,12 +3245,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "tbachert/spi", @@ -3496,16 +3500,16 @@ }, { "name": "utopia-php/agents", - "version": "dev-feat-embeddings", + "version": "0.5.0", "source": { "type": "git", "url": "https://github.com/utopia-php/agents.git", - "reference": "b448a4d06374c7a538c9495ac69a1a8f8e526823" + "reference": "7bbc77d4936ee7e87394a239690c9059b5cdaf1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/agents/zipball/b448a4d06374c7a538c9495ac69a1a8f8e526823", - "reference": "b448a4d06374c7a538c9495ac69a1a8f8e526823", + "url": "https://api.github.com/repos/utopia-php/agents/zipball/7bbc77d4936ee7e87394a239690c9059b5cdaf1b", + "reference": "7bbc77d4936ee7e87394a239690c9059b5cdaf1b", "shasum": "" }, "require": { @@ -3544,9 +3548,9 @@ ], "support": { "issues": "https://github.com/utopia-php/agents/issues", - "source": "https://github.com/utopia-php/agents/tree/feat-embeddings" + "source": "https://github.com/utopia-php/agents/tree/0.5.0" }, - "time": "2025-11-06T07:53:14+00:00" + "time": "2025-11-12T06:06:41+00:00" }, { "name": "utopia-php/analytics", @@ -3889,16 +3893,16 @@ }, { "name": "utopia-php/database", - "version": "dev-var_object", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "9a0cea65abbfb325506cb1d619db60dac7ee7e55" + "reference": "e10b4faa4f3a3ef30a5f6d76acdb605469924aec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/9a0cea65abbfb325506cb1d619db60dac7ee7e55", - "reference": "9a0cea65abbfb325506cb1d619db60dac7ee7e55", + "url": "https://api.github.com/repos/utopia-php/database/zipball/e10b4faa4f3a3ef30a5f6d76acdb605469924aec", + "reference": "e10b4faa4f3a3ef30a5f6d76acdb605469924aec", "shasum": "" }, "require": { @@ -3941,9 +3945,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/var_object" + "source": "https://github.com/utopia-php/database/tree/3.4.0" }, - "time": "2025-11-06T09:02:12+00:00" + "time": "2025-11-13T06:34:20+00:00" }, { "name": "utopia-php/detector", @@ -4508,12 +4512,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "573aae2840e0727844b6614e57fd0fffbfc51518" + "reference": "f5e175fa8da4bee6211dbeb4fe693a3eedb87672" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/573aae2840e0727844b6614e57fd0fffbfc51518", - "reference": "573aae2840e0727844b6614e57fd0fffbfc51518", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/f5e175fa8da4bee6211dbeb4fe693a3eedb87672", + "reference": "f5e175fa8da4bee6211dbeb4fe693a3eedb87672", "shasum": "" }, "require": { @@ -4522,7 +4526,7 @@ "ext-openssl": "*", "php": ">=8.1", "utopia-php/console": "0.0.*", - "utopia-php/database": "dev-var_object as 1.4.5", + "utopia-php/database": "3.*", "utopia-php/dsn": "0.2.*", "utopia-php/storage": "0.18.*" }, @@ -4555,7 +4559,7 @@ "issues": "https://github.com/utopia-php/migration/issues", "source": "https://github.com/utopia-php/migration/tree/feat-documents-db" }, - "time": "2025-10-28T15:06:08+00:00" + "time": "2025-11-13T11:11:30+00:00" }, { "name": "utopia-php/mongo", @@ -5427,16 +5431,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.5.1", + "version": "1.5.4", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "cd712674e34136f706e9170641ed6f4ce160e772" + "reference": "958947b6483a79e11c3812f23bb3056199fa4105" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/cd712674e34136f706e9170641ed6f4ce160e772", - "reference": "cd712674e34136f706e9170641ed6f4ce160e772", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/958947b6483a79e11c3812f23bb3056199fa4105", + "reference": "958947b6483a79e11c3812f23bb3056199fa4105", "shasum": "" }, "require": { @@ -5472,9 +5476,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.5.1" + "source": "https://github.com/appwrite/sdk-generator/tree/1.5.4" }, - "time": "2025-11-04T09:55:47+00:00" + "time": "2025-11-12T12:43:42+00:00" }, { "name": "doctrine/annotations", @@ -6178,16 +6182,16 @@ }, { "name": "phpbench/phpbench", - "version": "1.4.2", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/phpbench/phpbench.git", - "reference": "bb61ae6c54b3d58642be154eb09f4e73c3511018" + "reference": "b641dde59d969ea42eed70a39f9b51950bc96878" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/phpbench/zipball/bb61ae6c54b3d58642be154eb09f4e73c3511018", - "reference": "bb61ae6c54b3d58642be154eb09f4e73c3511018", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/b641dde59d969ea42eed70a39f9b51950bc96878", + "reference": "b641dde59d969ea42eed70a39f9b51950bc96878", "shasum": "" }, "require": { @@ -6202,26 +6206,26 @@ "phpbench/container": "^2.2", "psr/log": "^1.1 || ^2.0 || ^3.0", "seld/jsonlint": "^1.1", - "symfony/console": "^6.1 || ^7.0", - "symfony/filesystem": "^6.1 || ^7.0", - "symfony/finder": "^6.1 || ^7.0", - "symfony/options-resolver": "^6.1 || ^7.0", - "symfony/process": "^6.1 || ^7.0", + "symfony/console": "^6.1 || ^7.0 || ^8.0", + "symfony/filesystem": "^6.1 || ^7.0 || ^8.0", + "symfony/finder": "^6.1 || ^7.0 || ^8.0", + "symfony/options-resolver": "^6.1 || ^7.0 || ^8.0", + "symfony/process": "^6.1 || ^7.0 || ^8.0", "webmozart/glob": "^4.6" }, "require-dev": { "dantleech/invoke": "^2.0", "ergebnis/composer-normalize": "^2.39", - "friendsofphp/php-cs-fixer": "^3.0", "jangregor/phpstan-prophecy": "^1.0", + "php-cs-fixer/shim": "^3.9", "phpspec/prophecy": "^1.22", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^10.4 || ^11.0", "rector/rector": "^1.2", - "symfony/error-handler": "^6.1 || ^7.0", - "symfony/var-dumper": "^6.1 || ^7.0" + "symfony/error-handler": "^6.1 || ^7.0 || ^8.0", + "symfony/var-dumper": "^6.1 || ^7.0 || ^8.0" }, "suggest": { "ext-xdebug": "For Xdebug profiling extension." @@ -6264,7 +6268,7 @@ ], "support": { "issues": "https://github.com/phpbench/phpbench/issues", - "source": "https://github.com/phpbench/phpbench/tree/1.4.2" + "source": "https://github.com/phpbench/phpbench/tree/1.4.3" }, "funding": [ { @@ -6272,7 +6276,7 @@ "type": "github" } ], - "time": "2025-10-26T14:21:59+00:00" + "time": "2025-11-06T19:07:31+00:00" }, { "name": "phpstan/phpstan", @@ -7921,16 +7925,16 @@ }, { "name": "symfony/console", - "version": "v7.3.5", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7" + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cdb80fa5869653c83cfe1a9084a673b6daf57ea7", - "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7", + "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", "shasum": "" }, "require": { @@ -7995,7 +7999,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.5" + "source": "https://github.com/symfony/console/tree/v7.3.6" }, "funding": [ { @@ -8015,20 +8019,20 @@ "type": "tidelift" } ], - "time": "2025-10-14T15:46:26+00:00" + "time": "2025-11-04T01:21:42+00:00" }, { "name": "symfony/filesystem", - "version": "v7.3.2", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" + "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", - "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a", + "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a", "shasum": "" }, "require": { @@ -8065,7 +8069,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.2" + "source": "https://github.com/symfony/filesystem/tree/v7.3.6" }, "funding": [ { @@ -8085,7 +8089,7 @@ "type": "tidelift" } ], - "time": "2025-07-07T08:17:47+00:00" + "time": "2025-11-05T09:52:27+00:00" }, { "name": "symfony/finder", @@ -8940,12 +8944,6 @@ } ], "aliases": [ - { - "package": "utopia-php/database", - "version": "dev-var_object", - "alias": "1.4.5", - "alias_normalized": "1.4.5.0" - }, { "package": "utopia-php/migration", "version": "dev-feat-documents-db", @@ -8955,8 +8953,6 @@ ], "minimum-stability": "dev", "stability-flags": { - "utopia-php/agents": 20, - "utopia-php/database": 20, "utopia-php/migration": 20 }, "prefer-stable": true, diff --git a/src/Appwrite/Platform/Modules/Databases/Constants.php b/src/Appwrite/Platform/Modules/Databases/Constants.php index 34142b57b0..6ed373ef77 100644 --- a/src/Appwrite/Platform/Modules/Databases/Constants.php +++ b/src/Appwrite/Platform/Modules/Databases/Constants.php @@ -28,4 +28,4 @@ const DOCUMENTSDB = 'documentsdb'; const VECTORDB = 'vectordb'; const MIN_VECTOR_DIMENSION = 1; -const MAX_VECTOR_DIMENSION = 16000; \ No newline at end of file +const MAX_VECTOR_DIMENSION = 16000; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorDB/Collections/Create.php index 34e81dec3e..76a5a5d173 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorDB/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorDB/Collections/Create.php @@ -140,8 +140,8 @@ class Create extends CollectionAction attributes:$attributes, indexes:$indexes ); - // Create attribute metadata documents in the attributes table - // This is necessary so that indexes can find the attributes when they're created + // Create attribute and indexes metadata documents in the attributes and indexes collections + // needed for the get and list calls $attributeDocs = array_map(function ($attributeConfig) use ($database, $collection, $databaseId, $collectionId, $dimensions) { $key = \is_string($attributeConfig['$id']) ? $attributeConfig['$id'] : (string) $attributeConfig['$id']; return new Document([ @@ -165,6 +165,28 @@ class Create extends CollectionAction ]); }, $collections['defaultAttributes']); $dbForProject->createDocuments('attributes', $attributeDocs); + + $indexDocs = array_map(function ($indexConfig) use ($database, $collection, $databaseId, $collectionId) { + $key = \is_string($indexConfig['$id']) ? $indexConfig['$id'] : (string) $indexConfig['$id']; + + return new Document([ + '$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key), + 'key' => $key, + 'status' => 'available', + 'databaseInternalId' => $database->getSequence(), + 'databaseId' => $databaseId, + 'collectionInternalId' => $collection->getSequence(), + 'collectionId' => $collectionId, + 'type' => $indexConfig['type'], + 'attributes' => $indexConfig['attributes'] ?? [], + 'lengths' => $indexConfig['lengths'] ?? [], + 'orders' => $indexConfig['orders'] ?? [], + ]); + }, $collections['defaultIndexes']); + + if (!empty($indexDocs)) { + $dbForProject->createDocuments('indexes', $indexDocs); + } } catch (DuplicateException) { throw new Exception($this->getDuplicateException()); } catch (IndexException) { diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index c402092cf4..dc5cce9bd7 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -450,6 +450,7 @@ class Migrations extends Action { return match ($databaseType) { 'documentsdb' => $this->project->getAttribute('documentsDatabase'), + 'vectordb' => $this->project->getAttribute('vectorDatabase'), default => $this->project->getAttribute('database'), }; } diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index b46408f9f9..46594852df 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -7,6 +7,7 @@ use Tests\E2E\Client; use Tests\E2E\General\UsageTest; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Services\Functions\FunctionsBase; +use Utopia\Database\Database; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -82,6 +83,26 @@ trait MigrationsBase return $migrationResult; } + /** + * Get migration status by ID (without creating a new migration) + * + * @param string $migrationId + * @return array + */ + public function getMigrationStatus(string $migrationId): array + { + $response = $this->client->call(Client::METHOD_GET, '/migrations/' . $migrationId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + + return $response['body']; + } + /** * Appwrite E2E Migration Tests */ @@ -1494,6 +1515,200 @@ trait MigrationsBase ]; } + /** + * VectorDB (embeddings collections) + */ + public function testAppwriteMigrationVectorDBDatabase(): array + { + $response = $this->client->call(Client::METHOD_POST, '/vectordb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'databaseId' => ID::unique(), + 'name' => 'VDB - Migration DB' + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + + $databaseId = $response['body']['$id']; + + $result = $this->performMigrationSync([ + 'resources' => [ + Resource::TYPE_DATABASE_VECTORDB, + ], + 'endpoint' => 'http://localhost/v1', + 'projectId' => $this->getProject()['$id'], + 'apiKey' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals('completed', $result['status']); + $this->assertEquals([Resource::TYPE_DATABASE_VECTORDB], $result['resources']); + $this->assertArrayHasKey(Resource::TYPE_DATABASE_VECTORDB, $result['statusCounters']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_VECTORDB]['error'] ?? 0); + + $response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($databaseId, $response['body']['$id']); + $this->assertEquals('VDB - Migration DB', $response['body']['name']); + + // Cleanup on destination + $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + return [ + 'databaseId' => $databaseId, + ]; + } + + /** + * @depends testAppwriteMigrationVectorDBDatabase + */ + public function testAppwriteMigrationVectorDBCollection(array $data): array + { + $databaseId = $data['databaseId']; + + $collection = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'collectionId' => ID::unique(), + 'name' => 'VDB - Movies', + 'dimensions' => 3, + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + $collectionId = $collection['body']['$id']; + + $result = $this->performMigrationSync([ + 'resources' => [ + Resource::TYPE_DATABASE_VECTORDB, + Resource::TYPE_COLLECTION, + Resource::TYPE_ATTRIBUTE, + ], + 'endpoint' => 'http://localhost/v1', + 'projectId' => $this->getProject()['$id'], + 'apiKey' => $this->getProject()['apiKey'], + ]); + $this->assertEquals('completed', $result['status']); + + $response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $collectionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertEquals($collectionId, $response['body']['$id']); + $this->assertEquals('VDB - Movies', $response['body']['name']); + // Verify attributes are present (embeddings and metadata are default attributes) + $this->assertArrayHasKey('attributes', $response['body']); + $this->assertIsArray($response['body']['attributes']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + return [ + 'databaseId' => $databaseId, + 'collectionId' => $collectionId, + ]; + } + + /** + * @depends testAppwriteMigrationVectorDBCollection + */ + public function testAppwriteMigrationVectorDBDocument(array $data): void + { + $databaseId = $data['databaseId']; + $collectionId = $data['collectionId']; + + $document = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $collectionId . '/documents', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'documentId' => ID::unique(), + 'data' => [ + 'embeddings' => [1.0, 0.0, 0.0], + 'metadata' => ['title' => 'Migration Test Movie'], + ] + ]); + + $this->assertEquals(201, $document['headers']['status-code']); + $documentId = $document['body']['$id']; + + // Ensure attributes are exported before documents + $result = $this->performMigrationSync([ + 'resources' => [ + Resource::TYPE_DATABASE_VECTORDB, + Resource::TYPE_COLLECTION, + Resource::TYPE_ATTRIBUTE, + Resource::TYPE_DOCUMENT, + ], + 'endpoint' => 'http://localhost/v1', + 'projectId' => $this->getProject()['$id'], + 'apiKey' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals('completed', $result['status']); + // Verify that TYPE_ATTRIBUTE appears in the resources array for VectorDB + $this->assertContains(Resource::TYPE_ATTRIBUTE, $result['resources'], 'TYPE_ATTRIBUTE should be in resources array for VectorDB'); + + // Verify attributes exist on destination before checking document + $collectionResponse = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $collectionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + $this->assertEquals(200, $collectionResponse['headers']['status-code']); + $this->assertArrayHasKey('attributes', $collectionResponse['body']); + $this->assertIsArray($collectionResponse['body']['attributes']); + + $response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertEquals($documentId, $response['body']['$id']); + $this->assertEquals('Migration Test Movie', $response['body']['metadata']['title']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + } + /** * @depends testAppwriteMigrationDocumentsDBDatabase */ @@ -1688,6 +1903,31 @@ trait MigrationsBase $this->assertEquals('available', $response['body']['status']); }, 5000, 500); + $sqlIndexKey = 'product_unique'; + + $sqlIndex = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $sqlDatabaseId . '/tables/' . $tableId . '/indexes', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $sourceProject['$id'], + 'x-appwrite-key' => $sourceProject['apiKey'], + ], [ + 'key' => $sqlIndexKey, + 'type' => Database::INDEX_UNIQUE, + 'columns' => ['productName'], + ]); + + $this->assertEquals(202, $sqlIndex['headers']['status-code']); + + $this->assertEventually(function () use ($sqlDatabaseId, $tableId, $sqlIndexKey, $sourceProject) { + $index = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $sqlDatabaseId . '/tables/' . $tableId . '/indexes/' . $sqlIndexKey, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $sourceProject['$id'], + 'x-appwrite-key' => $sourceProject['apiKey'], + ]); + + $this->assertEquals(200, $index['headers']['status-code']); + $this->assertEquals('available', $index['body']['status']); + }, 30000, 500); + // Create Row in Table $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $sqlDatabaseId . '/tables/' . $tableId . '/rows', [ 'content-type' => 'application/json', @@ -1730,6 +1970,31 @@ trait MigrationsBase $this->assertEquals(201, $collection['headers']['status-code']); $collectionId = $collection['body']['$id']; + $documentsIndexKey = 'email_unique'; + + $documentsIndex = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $docsDatabaseId . '/collections/' . $collectionId . '/indexes', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $sourceProject['$id'], + 'x-appwrite-key' => $sourceProject['apiKey'], + ], [ + 'key' => $documentsIndexKey, + 'type' => Database::INDEX_UNIQUE, + 'attributes' => ['email'], + ]); + + $this->assertEquals(202, $documentsIndex['headers']['status-code']); + + $this->assertEventually(function () use ($docsDatabaseId, $collectionId, $documentsIndexKey, $sourceProject) { + $index = $this->client->call(Client::METHOD_GET, '/documentsdb/' . $docsDatabaseId . '/collections/' . $collectionId . '/indexes/' . $documentsIndexKey, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $sourceProject['$id'], + 'x-appwrite-key' => $sourceProject['apiKey'], + ]); + + $this->assertEquals(200, $index['headers']['status-code']); + $this->assertEquals('available', $index['body']['status']); + }, 30000, 500); + // Create Document in Collection $document = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $docsDatabaseId . '/collections/' . $collectionId . '/documents', [ 'content-type' => 'application/json', @@ -1746,8 +2011,119 @@ trait MigrationsBase $this->assertEquals(201, $document['headers']['status-code']); $documentId = $document['body']['$id']; - // ====== Perform migration including both database kinds with all child resources ====== - $result = $this->performMigrationSync([ + // ====== Create VectorDB (/vectordb) with collection and document ====== + $vector = $this->client->call(Client::METHOD_POST, '/vectordb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $sourceProject['$id'], + 'x-appwrite-key' => $sourceProject['apiKey'], + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Mixed VectorDB', + ]); + + $this->assertEquals(201, $vector['headers']['status-code']); + $this->assertNotEmpty($vector['body']['$id']); + $vectorDatabaseId = $vector['body']['$id']; + + // Create Collection in VectorDB + $vectorCollection = $this->client->call(Client::METHOD_POST, '/vectordb/' . $vectorDatabaseId . '/collections', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $sourceProject['$id'], + 'x-appwrite-key' => $sourceProject['apiKey'], + ], [ + 'collectionId' => ID::unique(), + 'name' => 'Products', + 'dimensions' => 3, + ]); + + $this->assertEquals(201, $vectorCollection['headers']['status-code']); + $vectorCollectionId = $vectorCollection['body']['$id']; + + // Wait for VectorDB collection attributes to be ready + $this->assertEventually(function () use ($vectorDatabaseId, $vectorCollectionId, $sourceProject) { + $response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $sourceProject['$id'], + 'x-appwrite-key' => $sourceProject['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertArrayHasKey('attributes', $response['body']); + $this->assertIsArray($response['body']['attributes']); + // Check that default attributes (embeddings and metadata) are present and ready + $attributeKeys = array_column($response['body']['attributes'], 'key'); + $this->assertContains('embeddings', $attributeKeys); + $this->assertContains('metadata', $attributeKeys); + // Check that attributes are available (if status field exists) + foreach ($response['body']['attributes'] as $attribute) { + if (isset($attribute['status']) && $attribute['status'] !== 'available') { + return false; + } + } + return true; + }, 10000, 500); + + $metadataIndexKey = '_key_metadata'; + $vectorIndexes = $this->client->call(Client::METHOD_GET, '/vectordb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId . '/indexes', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $sourceProject['$id'], + 'x-appwrite-key' => $sourceProject['apiKey'], + ]); + $this->assertEquals(200, $vectorIndexes['headers']['status-code']); + $metadataIndex = null; + foreach ($vectorIndexes['body']['indexes'] ?? [] as $index) { + if (($index['key'] ?? '') === $metadataIndexKey) { + $metadataIndex = $index; + break; + } + } + $this->assertNotNull($metadataIndex, 'Default metadata index should exist on source collection'); + $this->assertEquals(Database::INDEX_OBJECT, $metadataIndex['type']); + + $vectorEmbeddingIndexKey = 'embedding_euclidean'; + $vectorEmbeddingIndex = $this->client->call(Client::METHOD_POST, '/vectordb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId . '/indexes', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $sourceProject['$id'], + 'x-appwrite-key' => $sourceProject['apiKey'], + ], [ + 'key' => $vectorEmbeddingIndexKey, + 'type' => Database::INDEX_HNSW_EUCLIDEAN, + 'attributes' => ['embeddings'], + ]); + $this->assertEquals(202, $vectorEmbeddingIndex['headers']['status-code']); + + $this->assertEventually(function () use ($vectorDatabaseId, $vectorCollectionId, $vectorEmbeddingIndexKey, $sourceProject) { + $index = $this->client->call(Client::METHOD_GET, '/vectordb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId . '/indexes/' . $vectorEmbeddingIndexKey, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $sourceProject['$id'], + 'x-appwrite-key' => $sourceProject['apiKey'], + ]); + + $this->assertEquals(200, $index['headers']['status-code']); + $this->assertEquals(Database::INDEX_HNSW_EUCLIDEAN, $index['body']['type']); + if (isset($index['body']['status'])) { + $this->assertEquals('available', $index['body']['status']); + } + }, 30000, 500); + + // Create Document in VectorDB Collection + $vectorDocument = $this->client->call(Client::METHOD_POST, '/vectordb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId . '/documents', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $sourceProject['$id'], + 'x-appwrite-key' => $sourceProject['apiKey'], + ], [ + 'documentId' => ID::unique(), + 'data' => [ + 'embeddings' => [0.5, 0.3, 0.2], + 'metadata' => ['name' => 'Product Vector'], + ], + ]); + + $this->assertEquals(201, $vectorDocument['headers']['status-code']); + $vectorDocumentId = $vectorDocument['body']['$id']; + + // ====== Perform migration including all three database kinds with all child resources ====== + $migrationConfig = [ 'resources' => [ Resource::TYPE_DATABASE, Resource::TYPE_TABLE, @@ -1756,13 +2132,18 @@ trait MigrationsBase Resource::TYPE_DATABASE_DOCUMENTSDB, Resource::TYPE_COLLECTION, Resource::TYPE_DOCUMENT, + Resource::TYPE_DATABASE_VECTORDB, + Resource::TYPE_ATTRIBUTE, + Resource::TYPE_INDEX, ], 'endpoint' => 'http://localhost/v1', 'projectId' => $sourceProject['$id'], 'apiKey' => $sourceProject['apiKey'], - ]); + ]; - // ====== Assert migration result ====== + // Perform migration sync once and get migration ID + $result = $this->performMigrationSync($migrationConfig); + $migrationId = $result['$id']; $this->assertEquals('completed', $result['status']); $this->assertEquals('Appwrite', $result['source']); $this->assertEquals('Appwrite', $result['destination']); @@ -1774,8 +2155,13 @@ trait MigrationsBase Resource::TYPE_DATABASE_DOCUMENTSDB, Resource::TYPE_COLLECTION, Resource::TYPE_DOCUMENT, + Resource::TYPE_DATABASE_VECTORDB, + Resource::TYPE_ATTRIBUTE, + Resource::TYPE_INDEX, ], $result['resources']); + // Get migration status before asserting SQL Database counters + $result = $this->getMigrationStatus($migrationId); // Assert SQL Database counters $this->assertArrayHasKey(Resource::TYPE_DATABASE, $result['statusCounters']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE]['error']); @@ -1784,6 +2170,8 @@ trait MigrationsBase $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE]['processing']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE]['warning']); + // Get migration status before asserting Table counters + $result = $this->getMigrationStatus($migrationId); // Assert Table counters $this->assertArrayHasKey(Resource::TYPE_TABLE, $result['statusCounters']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TABLE]['error']); @@ -1792,6 +2180,8 @@ trait MigrationsBase $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TABLE]['processing']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TABLE]['warning']); + // Get migration status before asserting Column counters + $result = $this->getMigrationStatus($migrationId); // Assert Column counters $this->assertArrayHasKey(Resource::TYPE_COLUMN, $result['statusCounters']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLUMN]['error']); @@ -1800,6 +2190,8 @@ trait MigrationsBase $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLUMN]['processing']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLUMN]['warning']); + // Get migration status before asserting Row counters + $result = $this->getMigrationStatus($migrationId); // Assert Row counters $this->assertArrayHasKey(Resource::TYPE_ROW, $result['statusCounters']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ROW]['error']); @@ -1808,6 +2200,8 @@ trait MigrationsBase $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ROW]['processing']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ROW]['warning']); + // Get migration status before asserting DocumentsDB counters + $result = $this->getMigrationStatus($migrationId); // Assert DocumentsDB counters $this->assertArrayHasKey(Resource::TYPE_DATABASE_DOCUMENTSDB, $result['statusCounters']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_DOCUMENTSDB]['error']); @@ -1816,24 +2210,75 @@ trait MigrationsBase $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_DOCUMENTSDB]['processing']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_DOCUMENTSDB]['warning']); - // Assert Collection counters + // Wait for all collections to be fully processed and status counters to be updated + // Note: Collections are being transferred but status counters may not be updated immediately + // This wait ensures the migration worker has finished processing all collections + $result = null; + $this->assertEventually(function () use ($migrationId, &$result) { + $result = $this->getMigrationStatus($migrationId); + + // Check if collections status counters exist + if (!isset($result['statusCounters'][Resource::TYPE_COLLECTION])) { + return false; + } + + $pendingCount = $result['statusCounters'][Resource::TYPE_COLLECTION]['pending'] ?? 0; + + // Return true only when pending count is 0 + return $pendingCount === 0; + }, 30000, 1000); // 30 second timeout, check every 1 second + + // Assert Collection counters (covers both DocumentsDB and VectorDB collections) $this->assertArrayHasKey(Resource::TYPE_COLLECTION, $result['statusCounters']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLLECTION]['error']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLLECTION]['pending']); - $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_COLLECTION]['success']); + $this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_COLLECTION]['success']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLLECTION]['processing']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_COLLECTION]['warning']); - // Assert Document counters + // Get migration status before asserting Document counters + $result = $this->getMigrationStatus($migrationId); + // Assert Document counters (covers both DocumentsDB and VectorDB documents) $this->assertArrayHasKey(Resource::TYPE_DOCUMENT, $result['statusCounters']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DOCUMENT]['error']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DOCUMENT]['pending']); - $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_DOCUMENT]['success']); + $this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_DOCUMENT]['success']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DOCUMENT]['processing']); $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DOCUMENT]['warning']); - // Ensure only expected counters exist (7 total) - $this->assertCount(7, $result['statusCounters']); + // Get migration status before asserting VectorDB counters + $result = $this->getMigrationStatus($migrationId); + // Assert VectorDB counters + $this->assertArrayHasKey(Resource::TYPE_DATABASE_VECTORDB, $result['statusCounters']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_VECTORDB]['error']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_VECTORDB]['pending']); + $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_DATABASE_VECTORDB]['success']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_VECTORDB]['processing']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE_VECTORDB]['warning']); + + // Get migration status before asserting Attribute counters + $result = $this->getMigrationStatus($migrationId); + // Assert Attribute counters (for VectorDB) + $this->assertArrayHasKey(Resource::TYPE_ATTRIBUTE, $result['statusCounters']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ATTRIBUTE]['error']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ATTRIBUTE]['pending']); + $this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_ATTRIBUTE]['success']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ATTRIBUTE]['processing']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_ATTRIBUTE]['warning']); + + // Get migration status before asserting Index counters + $result = $this->getMigrationStatus($migrationId); + $this->assertArrayHasKey(Resource::TYPE_INDEX, $result['statusCounters']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_INDEX]['error']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_INDEX]['pending']); + $this->assertGreaterThanOrEqual(4, $result['statusCounters'][Resource::TYPE_INDEX]['success']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_INDEX]['processing']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_INDEX]['warning']); + + // Get migration status before asserting counter count + $result = $this->getMigrationStatus($migrationId); + // Ensure only expected counters exist (10 total) + $this->assertCount(10, $result['statusCounters']); // ====== Validate on destination: SQL Database resources ====== $response = $this->client->call(Client::METHOD_GET, '/databases/' . $sqlDatabaseId, [ @@ -1880,6 +2325,18 @@ trait MigrationsBase $this->assertEquals($rowId, $response['body']['$id']); $this->assertEquals('Laptop', $response['body']['productName']); + $sqlIndexDestination = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $sqlDatabaseId . '/tables/' . $tableId . '/indexes/' . $sqlIndexKey, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + $this->assertEquals(200, $sqlIndexDestination['headers']['status-code']); + $this->assertEquals($sqlIndexKey, $sqlIndexDestination['body']['key']); + $this->assertEquals(Database::INDEX_UNIQUE, $sqlIndexDestination['body']['type']); + if (isset($sqlIndexDestination['body']['columns'])) { + $this->assertEquals(['productName'], $sqlIndexDestination['body']['columns']); + } + // ====== Validate on destination: DocumentsDB resources ====== $response = $this->client->call(Client::METHOD_GET, '/documentsdb/' . $docsDatabaseId, [ 'content-type' => 'application/json', @@ -1914,7 +2371,72 @@ trait MigrationsBase $this->assertEquals('John Doe', $response['body']['name']); $this->assertEquals('john@example.com', $response['body']['email']); - // ====== Cleanup both destinations ====== + $documentsIndexDestination = $this->client->call(Client::METHOD_GET, '/documentsdb/' . $docsDatabaseId . '/collections/' . $collectionId . '/indexes/' . $documentsIndexKey, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + $this->assertEquals(200, $documentsIndexDestination['headers']['status-code']); + $this->assertEquals($documentsIndexKey, $documentsIndexDestination['body']['key']); + $this->assertEquals(Database::INDEX_UNIQUE, $documentsIndexDestination['body']['type']); + if (isset($documentsIndexDestination['body']['attributes'])) { + $this->assertEquals(['email'], $documentsIndexDestination['body']['attributes']); + } + + // ====== Validate on destination: VectorDB resources ====== + $response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $vectorDatabaseId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($vectorDatabaseId, $response['body']['$id']); + $this->assertEquals('Mixed VectorDB', $response['body']['name']); + + // Validate VectorDB Collection + $response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($vectorCollectionId, $response['body']['$id']); + $this->assertEquals('Products', $response['body']['name']); + // Verify attributes are present (embeddings and metadata are default attributes) + $this->assertArrayHasKey('attributes', $response['body']); + $this->assertIsArray($response['body']['attributes']); + + $vectorIndexesDestination = $this->client->call(Client::METHOD_GET, '/vectordb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId . '/indexes', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + $this->assertEquals(200, $vectorIndexesDestination['headers']['status-code']); + $indexByKey = []; + foreach ($vectorIndexesDestination['body']['indexes'] ?? [] as $index) { + if (isset($index['key'])) { + $indexByKey[$index['key']] = $index; + } + } + $this->assertArrayHasKey($metadataIndexKey, $indexByKey, 'Metadata index should exist on destination'); + $this->assertEquals(Database::INDEX_OBJECT, $indexByKey[$metadataIndexKey]['type']); + $this->assertArrayHasKey($vectorEmbeddingIndexKey, $indexByKey, 'Embeddings HNSW index should exist on destination'); + $this->assertEquals(Database::INDEX_HNSW_EUCLIDEAN, $indexByKey[$vectorEmbeddingIndexKey]['type']); + + // Validate VectorDB Document + $response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $vectorDatabaseId . '/collections/' . $vectorCollectionId . '/documents/' . $vectorDocumentId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($vectorDocumentId, $response['body']['$id']); + $this->assertEquals('Product Vector', $response['body']['metadata']['name']); + + // ====== Cleanup all destinations ====== $this->client->call(Client::METHOD_DELETE, '/databases/' . $sqlDatabaseId, [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getDestinationProject()['$id'], @@ -1927,6 +2449,12 @@ trait MigrationsBase 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); + $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $vectorDatabaseId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + // ====== Cleanup sources ====== $this->client->call(Client::METHOD_DELETE, '/databases/' . $sqlDatabaseId, [ 'content-type' => 'application/json', @@ -1939,5 +2467,11 @@ trait MigrationsBase 'x-appwrite-project' => $sourceProject['$id'], 'x-appwrite-key' => $sourceProject['apiKey'], ]); + + $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $vectorDatabaseId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $sourceProject['$id'], + 'x-appwrite-key' => $sourceProject['apiKey'], + ]); } }