From 130c2221ecfa3b183c4a51acd38fbff0dc52d23f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 3 Apr 2026 22:12:21 +0530 Subject: [PATCH 1/8] Fix VectorsDB metadata bootstrap race --- .../Http/VectorsDB/Collections/Create.php | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php index a7e2d68eac..787c7ae0d9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php @@ -62,7 +62,7 @@ class Create extends CollectionAction new SDKResponse( code: SwooleResponse::STATUS_CODE_CREATED, model: $this->getResponseModel(), - ) + ), ], contentType: ContentType::JSON )) @@ -72,7 +72,7 @@ class Create extends CollectionAction ->param('dimension', null, new Range(MIN_VECTOR_DIMENSION, MAX_VECTOR_DIMENSION), 'Embedding dimension.') ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) ->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) - ->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true) + ->param('enabled', true, new Boolean, 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true) ->inject('response') ->inject('dbForProject') ->inject('getDatabasesDB') @@ -95,7 +95,7 @@ class Create extends CollectionAction $permissions = Permission::aggregate($permissions) ?? []; try { - $collection = $dbForProject->createDocument('database_' . $database->getSequence(), new Document([ + $collection = $dbForProject->createDocument('database_'.$database->getSequence(), new Document([ '$id' => $collectionId, 'databaseInternalId' => $database->getSequence(), 'databaseId' => $databaseId, @@ -130,25 +130,27 @@ class Create extends CollectionAction $indexes[] = new Document($index); } try { - if (!$dbForDatabases->exists(null, Database::METADATA)) { - try { - $dbForDatabases->create(); - } catch (DuplicateException) { - } + // Bootstrap the database metadata without a separate existence + // check to avoid races when multiple first collections are created + // concurrently for the same VectorsDB database. + try { + $dbForDatabases->create(); + } catch (DuplicateException) { } $dbForDatabases->createCollection( - id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + id: 'database_'.$database->getSequence().'_collection_'.$collection->getSequence(), permissions: $permissions, documentSecurity: $documentSecurity, - attributes:$attributes, - indexes:$indexes + attributes: $attributes, + indexes: $indexes ); // 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, $dimension) { $key = \is_string($attributeConfig['$id']) ? $attributeConfig['$id'] : (string) $attributeConfig['$id']; + return new Document([ - '$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key), + '$id' => ID::custom($database->getSequence().'_'.$collection->getSequence().'_'.$key), 'key' => $key, 'databaseInternalId' => $database->getSequence(), 'databaseId' => $databaseId, @@ -173,7 +175,7 @@ class Create extends CollectionAction $key = \is_string($indexConfig['$id']) ? $indexConfig['$id'] : (string) $indexConfig['$id']; return new Document([ - '$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key), + '$id' => ID::custom($database->getSequence().'_'.$collection->getSequence().'_'.$key), 'key' => $key, 'status' => 'available', 'databaseInternalId' => $database->getSequence(), @@ -187,7 +189,7 @@ class Create extends CollectionAction ]); }, $collections['defaultIndexes']); - if (!empty($indexDocs)) { + if (! empty($indexDocs)) { $dbForProject->createDocuments('indexes', $indexDocs); } } catch (DuplicateException) { From a5f45b46e9dfbff8ec640cc9dd3ba7e4b708ec88 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 3 Apr 2026 23:41:44 +0530 Subject: [PATCH 2/8] Handle raced VectorsDB metadata bootstrap errors --- .../Http/VectorsDB/Collections/Create.php | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php index 787c7ae0d9..d03213d4b8 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php @@ -72,7 +72,7 @@ class Create extends CollectionAction ->param('dimension', null, new Range(MIN_VECTOR_DIMENSION, MAX_VECTOR_DIMENSION), 'Embedding dimension.') ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) ->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) - ->param('enabled', true, new Boolean, 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true) + ->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true) ->inject('response') ->inject('dbForProject') ->inject('getDatabasesDB') @@ -133,9 +133,23 @@ class Create extends CollectionAction // Bootstrap the database metadata without a separate existence // check to avoid races when multiple first collections are created // concurrently for the same VectorsDB database. - try { - $dbForDatabases->create(); - } catch (DuplicateException) { + for ($attempt = 0; $attempt < 5; $attempt++) { + try { + $dbForDatabases->create(); + break; + } catch (DuplicateException) { + break; + } catch (\Throwable $e) { + if ($dbForDatabases->exists(null, Database::METADATA)) { + break; + } + + if ($attempt === 4) { + throw $e; + } + + \usleep(100_000); + } } $dbForDatabases->createCollection( id: 'database_'.$database->getSequence().'_collection_'.$collection->getSequence(), From 3cb53f06047e3411893b45b49ef4671e3fe2ea85 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 3 Apr 2026 23:43:51 +0530 Subject: [PATCH 3/8] Drop unrelated formatting churn from VectorsDB fix --- .../Http/VectorsDB/Collections/Create.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php index d03213d4b8..0294790a9e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php @@ -62,7 +62,7 @@ class Create extends CollectionAction new SDKResponse( code: SwooleResponse::STATUS_CODE_CREATED, model: $this->getResponseModel(), - ), + ) ], contentType: ContentType::JSON )) @@ -95,7 +95,7 @@ class Create extends CollectionAction $permissions = Permission::aggregate($permissions) ?? []; try { - $collection = $dbForProject->createDocument('database_'.$database->getSequence(), new Document([ + $collection = $dbForProject->createDocument('database_' . $database->getSequence(), new Document([ '$id' => $collectionId, 'databaseInternalId' => $database->getSequence(), 'databaseId' => $databaseId, @@ -152,11 +152,11 @@ class Create extends CollectionAction } } $dbForDatabases->createCollection( - id: 'database_'.$database->getSequence().'_collection_'.$collection->getSequence(), + id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), permissions: $permissions, documentSecurity: $documentSecurity, - attributes: $attributes, - indexes: $indexes + attributes:$attributes, + indexes:$indexes ); // Create attribute and indexes metadata documents in the attributes and indexes collections // needed for the get and list calls @@ -164,7 +164,7 @@ class Create extends CollectionAction $key = \is_string($attributeConfig['$id']) ? $attributeConfig['$id'] : (string) $attributeConfig['$id']; return new Document([ - '$id' => ID::custom($database->getSequence().'_'.$collection->getSequence().'_'.$key), + '$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key), 'key' => $key, 'databaseInternalId' => $database->getSequence(), 'databaseId' => $databaseId, @@ -189,7 +189,7 @@ class Create extends CollectionAction $key = \is_string($indexConfig['$id']) ? $indexConfig['$id'] : (string) $indexConfig['$id']; return new Document([ - '$id' => ID::custom($database->getSequence().'_'.$collection->getSequence().'_'.$key), + '$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key), 'key' => $key, 'status' => 'available', 'databaseInternalId' => $database->getSequence(), @@ -203,7 +203,7 @@ class Create extends CollectionAction ]); }, $collections['defaultIndexes']); - if (! empty($indexDocs)) { + if (!empty($indexDocs)) { $dbForProject->createDocuments('indexes', $indexDocs); } } catch (DuplicateException) { From f3f2855fe5e74917cba658799ae3250fcecf4516 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 3 Apr 2026 23:44:19 +0530 Subject: [PATCH 4/8] Remove final formatting-only diff --- .../Modules/Databases/Http/VectorsDB/Collections/Create.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php index 0294790a9e..58433c7deb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php @@ -162,7 +162,6 @@ class Create extends CollectionAction // needed for the get and list calls $attributeDocs = array_map(function ($attributeConfig) use ($database, $collection, $databaseId, $collectionId, $dimension) { $key = \is_string($attributeConfig['$id']) ? $attributeConfig['$id'] : (string) $attributeConfig['$id']; - return new Document([ '$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key), 'key' => $key, From c978b6f34f7a18a740cdec915f2de0b895f1ab81 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 3 Apr 2026 23:58:25 +0530 Subject: [PATCH 5/8] Stabilize function deployment activation in tests --- tests/e2e/Services/Functions/FunctionsBase.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index af426d5221..42976cda84 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -100,6 +100,24 @@ trait FunctionsBase 'x-appwrite-key' => $this->getProject()['apiKey'], ])); $this->assertNotEquals(401, $function['headers']['status-code'], 'Auth failed while polling function activation'); + + if ( + ($function['body']['deploymentId'] ?? '') !== $deploymentId + && ($function['body']['latestDeploymentId'] ?? '') === $deploymentId + && ($function['body']['latestDeploymentStatus'] ?? '') === 'ready' + ) { + $activation = $this->updateFunctionDeployment($functionId, $deploymentId); + $this->assertContains( + $activation['headers']['status-code'], + [200, 409], + 'Deployment activation request failed: ' . json_encode($activation['body'], JSON_PRETTY_PRINT) + ); + + if ($activation['headers']['status-code'] === 200) { + $function = $activation; + } + } + $this->assertEquals($deploymentId, $function['body']['deploymentId'] ?? '', 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT)); }, 120000, 500); } From 66e68aea143eda00130c3b793960940b788eb4cd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 5 Apr 2026 19:37:29 +0530 Subject: [PATCH 6/8] fix: fail specs when docs are missing --- app/cli.php | 6 +++++- src/Appwrite/SDK/Specification/Format.php | 19 +++++++++++++++++++ .../SDK/Specification/Format/OpenAPI3.php | 4 ++-- .../SDK/Specification/Format/Swagger2.php | 4 ++-- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/app/cli.php b/app/cli.php index b8721320be..b636707f1c 100644 --- a/app/cli.php +++ b/app/cli.php @@ -329,17 +329,20 @@ $setResource('bus', function (Registry $register) use ($cli) { $setResource('telemetry', fn () => new NoTelemetry(), []); +$exitCode = 0; + $cli ->error() ->inject('error') ->inject('logError') - ->action(function (Throwable $error, callable $logError) use ($taskName) { + ->action(function (Throwable $error, callable $logError) use ($taskName, &$exitCode) { call_user_func_array($logError, [ $error, 'Task', $taskName, ]); + $exitCode = 1; Timer::clearAll(); }); @@ -348,3 +351,4 @@ $cli->shutdown()->action(fn () => Timer::clearAll()); Runtime::enableCoroutine(SWOOLE_HOOK_ALL); require_once __DIR__ . '/init/span.php'; run($cli->run(...)); +Console::exit($exitCode); diff --git a/src/Appwrite/SDK/Specification/Format.php b/src/Appwrite/SDK/Specification/Format.php index 7a867c5b91..dd4d378345 100644 --- a/src/Appwrite/SDK/Specification/Format.php +++ b/src/Appwrite/SDK/Specification/Format.php @@ -210,6 +210,25 @@ abstract class Format return $this->services; } + protected function getDescriptionContents(?string $description): string + { + if ($description === null || $description === '') { + return ''; + } + + if (!\str_ends_with($description, '.md')) { + return $description; + } + + $contents = @\file_get_contents($description); + + if ($contents === false) { + throw new \RuntimeException('Documentation file not found or unreadable: ' . $description); + } + + return $contents; + } + protected function getRequestEnumName(string $service, string $method, string $param): ?string { /* `$service` is `$namespace` */ diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 753a0dc52f..41ed386e30 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -128,7 +128,7 @@ class OpenAPI3 extends Format if ($desc === null) { $desc = ''; } - $descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc; + $descContents = $this->getDescriptionContents($desc); $temp = [ 'summary' => $route->getDesc(), @@ -193,7 +193,7 @@ class OpenAPI3 extends Format 'parameters' => [], 'required' => [], 'responses' => [], - 'description' => ($desc) ? \file_get_contents($desc) : '', + 'description' => $this->getDescriptionContents($desc), 'demo' => \strtolower($namespace) . '/' . Template::fromCamelCaseToDash($methodObj->getMethodName()) . '.md', 'public' => $methodObj->isPublic(), ]; diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index 3e9ac891fa..dc65bea215 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -129,7 +129,7 @@ class Swagger2 extends Format if ($desc === null) { $desc = ''; } - $descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc; + $descContents = $this->getDescriptionContents($desc); $temp = [ 'summary' => $route->getDesc(), @@ -201,7 +201,7 @@ class Swagger2 extends Format 'parameters' => [], 'required' => [], 'responses' => [], - 'description' => ($desc) ? \file_get_contents($desc) : '', + 'description' => $this->getDescriptionContents($desc), 'demo' => \strtolower($namespace) . '/' . Template::fromCamelCaseToDash($methodObj->getMethodName()) . '.md', 'public' => $methodObj->isPublic(), ]; From 5ab28ad99acaf946db36438cdbe126ba6ebf18f7 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 5 Apr 2026 19:52:48 +0530 Subject: [PATCH 7/8] docs: add missing json migration references --- docs/references/migrations/migration-json-export.md | 1 + docs/references/migrations/migration-json-import.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 docs/references/migrations/migration-json-export.md create mode 100644 docs/references/migrations/migration-json-import.md diff --git a/docs/references/migrations/migration-json-export.md b/docs/references/migrations/migration-json-export.md new file mode 100644 index 0000000000..8a955c5990 --- /dev/null +++ b/docs/references/migrations/migration-json-export.md @@ -0,0 +1 @@ +Export documents to a JSON file from your Appwrite database. This endpoint allows you to export documents to a JSON file stored in a secure internal bucket. You'll receive an email with a download link when the export is complete. diff --git a/docs/references/migrations/migration-json-import.md b/docs/references/migrations/migration-json-import.md new file mode 100644 index 0000000000..2eeeaf5619 --- /dev/null +++ b/docs/references/migrations/migration-json-import.md @@ -0,0 +1 @@ +Import documents from a JSON file into your Appwrite database. This endpoint allows you to import documents from a JSON file uploaded to Appwrite Storage bucket. From 5d1da00138c87a7b9e6c082a5feb6413ce57ee92 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 5 Apr 2026 20:12:25 +0530 Subject: [PATCH 8/8] refactor: remove redundant desc guards --- src/Appwrite/SDK/Specification/Format/OpenAPI3.php | 3 --- src/Appwrite/SDK/Specification/Format/Swagger2.php | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 41ed386e30..88f577eac6 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -125,9 +125,6 @@ class OpenAPI3 extends Format $namespace = $sdk->getNamespace() ?? 'default'; - if ($desc === null) { - $desc = ''; - } $descContents = $this->getDescriptionContents($desc); $temp = [ diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index dc65bea215..f9c79431f0 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -126,9 +126,6 @@ class Swagger2 extends Format $sdkPlatforms = array_values(array_unique($sdkPlatforms)); $namespace = $sdk->getNamespace() ?? 'default'; - if ($desc === null) { - $desc = ''; - } $descContents = $this->getDescriptionContents($desc); $temp = [