updated migrations + index finding issue in vectordb

This commit is contained in:
ArnabChatterjee20k
2025-11-13 16:47:00 +05:30
parent 75045ffd58
commit 93bd6b39a4
6 changed files with 651 additions and 98 deletions
+2 -2
View File
@@ -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.*",
Generated
+78 -82
View File
@@ -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,
@@ -28,4 +28,4 @@ const DOCUMENTSDB = 'documentsdb';
const VECTORDB = 'vectordb';
const MIN_VECTOR_DIMENSION = 1;
const MAX_VECTOR_DIMENSION = 16000;
const MAX_VECTOR_DIMENSION = 16000;
@@ -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) {
@@ -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'),
};
}
+545 -11
View File
@@ -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'],
]);
}
}