diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php index caa80c74d2..1682fedcfa 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php @@ -119,36 +119,55 @@ class Create extends Action throw new Exception(Exception::DATABASE_NOT_FOUND); } + $collectionKey = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(); + $databaseKey = 'database_' . $database->getSequence(); + $collectionAttributes = []; $attributeDocuments = []; - foreach ($attributes as $attributeDef) { - $attrDoc = $this->buildAttributeDocument($database, $collection, $attributeDef, $dbForProject); - $collectionAttributes[] = $attrDoc['collection']; - $attributeDocuments[] = $attrDoc['document']; + try { + foreach ($attributes as $attributeDef) { + $attrDoc = $this->buildAttributeDocument($database, $collection, $attributeDef, $dbForProject); + $collectionAttributes[] = $attrDoc['collection']; + $attributeDocuments[] = $attrDoc['document']; + } + } catch (\Throwable $e) { + $dbForProject->deleteDocument($databaseKey, $collection->getId()); + throw $e; } $collectionIndexes = []; $indexDocuments = []; - foreach ($indexes as $indexDef) { - $idxDoc = $this->buildIndexDocument($database, $collection, $indexDef, $collectionAttributes); - $collectionIndexes[] = $idxDoc['collection']; - $indexDocuments[] = $idxDoc['document']; + try { + foreach ($indexes as $indexDef) { + $idxDoc = $this->buildIndexDocument($database, $collection, $indexDef, $collectionAttributes); + $collectionIndexes[] = $idxDoc['collection']; + $indexDocuments[] = $idxDoc['document']; + } + } catch (\Throwable $e) { + $dbForProject->deleteDocument($databaseKey, $collection->getId()); + throw $e; } try { $dbForProject->createCollection( - id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), + id: $collectionKey, attributes: $collectionAttributes, indexes: $collectionIndexes, permissions: $permissions, documentSecurity: $documentSecurity ); } catch (DuplicateException) { + $dbForProject->deleteDocument($databaseKey, $collection->getId()); throw new Exception($this->getDuplicateException()); } catch (IndexException $e) { + $dbForProject->deleteDocument($databaseKey, $collection->getId()); throw new Exception($this->getInvalidIndexException(), $e->getMessage()); } catch (LimitException) { + $dbForProject->deleteDocument($databaseKey, $collection->getId()); throw new Exception($this->getLimitException()); + } catch (\Throwable $e) { + $dbForProject->deleteDocument($databaseKey, $collection->getId()); + throw $e; } // Create documents in attributes and indexes collections @@ -160,7 +179,11 @@ class Create extends Action $dbForProject->createDocuments('indexes', $indexDocuments); } } catch (DuplicateException) { + $this->cleanup($dbForProject, $databaseKey, $collectionKey, $collection->getId()); throw new Exception($this->getDuplicateException()); + } catch (\Throwable $e) { + $this->cleanup($dbForProject, $databaseKey, $collectionKey, $collection->getId()); + throw $e; } $dbForProject->purgeCachedDocument('database_' . $database->getSequence(), $collection->getId()); @@ -362,4 +385,22 @@ class Create extends Action 'document' => $document, ]; } + + /** + * Cleanup on failure: delete the collection document and the underlying DB collection + */ + protected function cleanup(Database $dbForProject, string $databaseKey, string $collectionKey, string $collectionId): void + { + try { + $dbForProject->deleteCollection($collectionKey); + } catch (\Throwable) { + // Ignore cleanup errors for collection deletion + } + + try { + $dbForProject->deleteDocument($databaseKey, $collectionId); + } catch (\Throwable) { + // Ignore cleanup errors for document deletion + } + } } diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php b/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php index 7796f2c2f0..55dffb2c34 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php @@ -7110,6 +7110,76 @@ class DatabasesCustomServerTest extends Scope ])); } + public function testCreateCollectionCleanupOnFailure(): void + { + // Create database + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'Test Cleanup', + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + $collectionId = ID::unique(); + + // Test: Create collection with invalid index referencing non-existent attribute (should fail) + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => $collectionId, + 'name' => 'Should Fail', + 'attributes' => [ + [ + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 256, + ], + ], + 'indexes' => [ + [ + 'key' => 'idx_invalid', + 'type' => Database::INDEX_KEY, + 'attributes' => ['nonexistent'], + ], + ], + ]); + + $this->assertEquals(400, $collection['headers']['status-code']); + + // Verify collection was cleaned up - creating with same ID should succeed + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => $collectionId, + 'name' => 'Should Succeed', + 'attributes' => [ + [ + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 256, + ], + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + } + public function testCreateCollectionWithEnumAttribute(): void { // Create database