diff --git a/composer.lock b/composer.lock index 0ed0d05dc5..d8c17fbc91 100644 --- a/composer.lock +++ b/composer.lock @@ -1352,16 +1352,16 @@ }, { "name": "open-telemetry/gen-otlp-protobuf", - "version": "1.5.0", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git", - "reference": "585bafddd4ae6565de154610b10a787a455c9ba0" + "reference": "673af5b06545b513466081884b47ef15a536edde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/585bafddd4ae6565de154610b10a787a455c9ba0", - "reference": "585bafddd4ae6565de154610b10a787a455c9ba0", + "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/673af5b06545b513466081884b47ef15a536edde", + "reference": "673af5b06545b513466081884b47ef15a536edde", "shasum": "" }, "require": { @@ -1411,7 +1411,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-01-15T23:07:07+00:00" + "time": "2025-09-17T23:10:12+00:00" }, { "name": "open-telemetry/sdk", @@ -3635,16 +3635,16 @@ }, { "name": "utopia-php/database", - "version": "1.4.9", + "version": "1.4.10", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "066e2bda7b728bb843776db3640737d7350ba035" + "reference": "5514bb7346e75996d061d08040248fe842f73785" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/066e2bda7b728bb843776db3640737d7350ba035", - "reference": "066e2bda7b728bb843776db3640737d7350ba035", + "url": "https://api.github.com/repos/utopia-php/database/zipball/5514bb7346e75996d061d08040248fe842f73785", + "reference": "5514bb7346e75996d061d08040248fe842f73785", "shasum": "" }, "require": { @@ -3685,9 +3685,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/1.4.9" + "source": "https://github.com/utopia-php/database/tree/1.4.10" }, - "time": "2025-09-16T13:31:52+00:00" + "time": "2025-09-18T02:42:25+00:00" }, { "name": "utopia-php/detector", @@ -5278,16 +5278,16 @@ }, { "name": "laravel/pint", - "version": "v1.24.0", + "version": "v1.25.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a" + "reference": "595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a", - "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "url": "https://api.github.com/repos/laravel/pint/zipball/595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96", + "reference": "595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96", "shasum": "" }, "require": { @@ -5298,9 +5298,9 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.82.2", - "illuminate/view": "^11.45.1", - "larastan/larastan": "^3.5.0", + "friendsofphp/php-cs-fixer": "^3.87.2", + "illuminate/view": "^11.46.0", + "larastan/larastan": "^3.7.1", "laravel-zero/framework": "^11.45.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.3.1", @@ -5311,9 +5311,6 @@ ], "type": "project", "autoload": { - "files": [ - "overrides/Runner/Parallel/ProcessFactory.php" - ], "psr-4": { "App\\": "app/", "Database\\Seeders\\": "database/seeders/", @@ -5343,7 +5340,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-07-10T18:09:32+00:00" + "time": "2025-09-17T01:36:44+00:00" }, { "name": "matthiasmullie/minify", diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php index f3903d91a7..22a90d2653 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php @@ -122,7 +122,7 @@ abstract class Action extends UtopiaAction /** * Get the correct invalid structure message. */ - protected function getInvalidStructureException(): string + protected function getStructureException(): string { return $this->isCollectionsAPI() ? Exception::DOCUMENT_INVALID_STRUCTURE @@ -366,13 +366,27 @@ abstract class Action extends UtopiaAction 'filters' => $filters, 'options' => $options, ]); + if ( + !$dbForProject->getAdapter()->getSupportForSpatialIndexNull() && + \in_array($attribute->getAttribute('type'), Database::SPATIAL_TYPES) && + $attribute->getAttribute('required') + ) { + $hasData = !Authorization::skip(fn () => $dbForProject + ->findOne('database_' . $db->getSequence() . '_collection_' . $collection->getSequence())) + ->isEmpty(); + if ($hasData) { + throw new StructureException('Failed to add required spatial column: existing rows present. Make the column optional.'); + } + } $dbForProject->checkAttribute($collection, $attribute); $attribute = $dbForProject->createDocument('attributes', $attribute); } catch (DuplicateException) { throw new Exception($this->getDuplicateException()); } catch (LimitException) { throw new Exception($this->getLimitException()); + } catch (StructureException $e) { + throw new Exception($this->getStructureException(), $e->getMessage()); } catch (Throwable $e) { $dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId); $dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $collection->getSequence()); @@ -416,7 +430,7 @@ abstract class Action extends UtopiaAction } catch (LimitException) { throw new Exception($this->getLimitException()); } catch (StructureException) { - throw new Exception($this->getInvalidStructureException()); + throw new Exception($this->getStructureException()); } catch (Throwable $e) { $dbForProject->deleteDocument('attributes', $attribute->getId()); throw $e; @@ -580,7 +594,7 @@ abstract class Action extends UtopiaAction } catch (RelationshipException $e) { throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); } catch (StructureException $e) { - throw new Exception($this->getInvalidStructureException(), $e->getMessage()); + throw new Exception($this->getStructureException(), $e->getMessage()); } if ($primaryDocumentOptions['twoWay']) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index d1d0738990..e05e588201 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -160,7 +160,7 @@ abstract class Action extends AppwriteAction /** * Get the correct invalid structure message. */ - protected function getInvalidStructureException(): string + protected function getStructureException(): string { return $this->isCollectionsAPI() ? Exception::DOCUMENT_INVALID_STRUCTURE diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php index 0f0ae14020..baab45cdd3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php @@ -149,7 +149,7 @@ class Update extends Action } catch (RelationshipException $e) { throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); } catch (StructureException $e) { - throw new Exception($this->getInvalidStructureException(), $e->getMessage()); + throw new Exception($this->getStructureException(), $e->getMessage()); } foreach ($documents as $document) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php index 395e3d757b..4ce3990a38 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php @@ -130,7 +130,7 @@ class Upsert extends Action } catch (RelationshipException $e) { throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); } catch (StructureException $e) { - throw new Exception($this->getInvalidStructureException(), $e->getMessage()); + throw new Exception($this->getStructureException(), $e->getMessage()); } foreach ($upserted as $document) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index c03daabd4f..a8af1eda86 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -375,7 +375,7 @@ class Create extends Action } catch (RelationshipException $e) { throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); } catch (StructureException $e) { - throw new Exception($this->getInvalidStructureException(), $e->getMessage()); + throw new Exception($this->getStructureException(), $e->getMessage()); } $queueForEvents diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php index 8382bdd5e9..e510aeb089 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php @@ -247,7 +247,7 @@ class Update extends Action } catch (RelationshipException $e) { throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); } catch (StructureException $e) { - throw new Exception($this->getInvalidStructureException(), $e->getMessage()); + throw new Exception($this->getStructureException(), $e->getMessage()); } $collectionsCache = []; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php index 54b1cad950..3ac5e704e3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php @@ -258,7 +258,7 @@ class Upsert extends Action } catch (RelationshipException $e) { throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); } catch (StructureException $e) { - throw new Exception($this->getInvalidStructureException(), $e->getMessage()); + throw new Exception($this->getStructureException(), $e->getMessage()); } $collectionsCache = []; diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php index 8d27aa7230..8b4f78d3ea 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php @@ -7798,4 +7798,269 @@ trait DatabasesBase 'x-appwrite-key' => $this->getProject()['apiKey'] ])); } + + public function testSpatialColCreateOnExistingData(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Distance Meters Database' + ]); + + $databaseId = $database['body']['$id']; + + $colId = ID::unique(); + $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' => $colId, + 'name' => 'spatial-test', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + $description = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $colId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'description', + 'size' => 512, + 'required' => false, + 'default' => '', + ]); + + $this->assertEquals(202, $description['headers']['status-code']); + sleep(2); + + $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $colId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'description' => 'description' + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $document['headers']['status-code']); + + $point = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $colId . '/attributes/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'loc', + 'required' => true, + ]); + + $this->assertEquals(400, $point['headers']['status-code']); + + $point = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $colId . '/attributes/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'loc', + 'required' => false, + 'default' => null + ]); + + $this->assertEquals(202, $point['headers']['status-code']); + + $line = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $colId . '/attributes/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'route', + 'required' => true, + ]); + + $this->assertEquals(400, $line['headers']['status-code']); + + $line = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $colId . '/attributes/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'route', + 'required' => false, + 'default' => null + ]); + + $this->assertEquals(202, $line['headers']['status-code']); + + $poly = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $colId . '/attributes/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'area', + 'required' => true, + ]); + + $this->assertEquals(400, $poly['headers']['status-code']); + + $poly = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $colId . '/attributes/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'area', + 'required' => false, + 'default' => null + ]); + + $this->assertEquals(202, $poly['headers']['status-code']); + } + + public function testSpatialColCreateOnExistingDataWithDefaults(): void + { + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial With Defaults Database' + ]); + + $databaseId = $database['body']['$id']; + + $colId = ID::unique(); + $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' => $colId, + 'name' => 'spatial-test-defaults', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + + $description = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $colId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'description', + 'size' => 512, + 'required' => false, + 'default' => '', + ]); + + $this->assertEquals(202, $description['headers']['status-code']); + sleep(2); + + $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $colId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'description' => 'description' + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $document['headers']['status-code']); + + // Test point with default value + $point = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $colId . '/attributes/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'loc', + 'required' => false, + 'default' => [0.0, 0.0] + ]); + + $this->assertEquals(202, $point['headers']['status-code']); + + // Test line with default value + $line = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $colId . '/attributes/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'route', + 'required' => false, + 'default' => [[0.0, 0.0], [1.0, 1.0]] + ]); + + $this->assertEquals(202, $line['headers']['status-code']); + + // Test polygon with default value + $poly = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $colId . '/attributes/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'area', + 'required' => false, + 'default' => [[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]]] + ]); + + $this->assertEquals(202, $poly['headers']['status-code']); + + // Wait for attributes to be available + sleep(2); + + // Create a new document without spatial data to test default values + $newDocument = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $colId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'description' => 'test default values' + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $newDocument['headers']['status-code']); + + $newDocumentId = $newDocument['body']['$id']; + + // Fetch the document to verify default values are applied + $fetchedDocument = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $colId . '/documents/' . $newDocumentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $fetchedDocument['headers']['status-code']); + + // Verify default values are applied + $this->assertEquals([0.0, 0.0], $fetchedDocument['body']['loc']); + $this->assertEquals([[0.0, 0.0], [1.0, 1.0]], $fetchedDocument['body']['route']); + $this->assertEquals([[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]]], $fetchedDocument['body']['area']); + } } diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php index eba7ec96a7..387a7ec3f0 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -3062,7 +3062,7 @@ trait DatabasesBase public function testInvalidRowStructure(): void { - $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3735,7 +3735,7 @@ trait DatabasesBase public function testEnforceTableAndRowPermissions(): void { - $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -3928,7 +3928,7 @@ trait DatabasesBase public function testEnforceTablePermissions(): void { - $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -4284,7 +4284,7 @@ trait DatabasesBase public function testUpdatePermissionsWithEmptyPayload(): array { // Create Database - $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -8828,4 +8828,269 @@ trait DatabasesBase $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}", $headers); } + public function testSpatialColCreateOnExistingData(): void + { + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial Distance Meters Database' + ]); + + $databaseId = $database['body']['$id']; + + $tableId = ID::unique(); + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => $tableId, + 'name' => 'spatial-test', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + + $description = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'description', + 'size' => 512, + 'required' => false, + 'default' => '', + ]); + + $this->assertEquals(202, $description['headers']['status-code']); + sleep(2); + + $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'description' => 'description' + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $row['headers']['status-code']); + + $point = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'loc', + 'required' => true, + ]); + + $this->assertEquals(400, $point['headers']['status-code']); + + $point = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'loc', + 'required' => false, + 'default' => null + ]); + + $this->assertEquals(202, $point['headers']['status-code']); + + $line = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'route', + 'required' => true, + ]); + + $this->assertEquals(400, $line['headers']['status-code']); + + $line = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'route', + 'required' => false, + 'default' => null + ]); + + $this->assertEquals(202, $line['headers']['status-code']); + + $poly = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'area', + 'required' => true, + ]); + + $this->assertEquals(400, $poly['headers']['status-code']); + + $poly = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'area', + 'required' => false, + 'default' => null + ]); + + $this->assertEquals(202, $poly['headers']['status-code']); + } + + public function testSpatialColCreateOnExistingDataWithDefaults(): void + { + $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Spatial With Defaults Database' + ]); + + $databaseId = $database['body']['$id']; + + $tableId = ID::unique(); + $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => $tableId, + 'name' => 'spatial-test-defaults', + 'rowSecurity' => true, + 'permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], + ]); + + $this->assertEquals(201, $table['headers']['status-code']); + + $description = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'description', + 'size' => 512, + 'required' => false, + 'default' => '', + ]); + + $this->assertEquals(202, $description['headers']['status-code']); + sleep(2); + + $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'description' => 'description' + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $row['headers']['status-code']); + + // Test point with default value + $point = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/point', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'loc', + 'required' => false, + 'default' => [0.0, 0.0] + ]); + + $this->assertEquals(202, $point['headers']['status-code']); + + // Test line with default value + $line = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/line', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'route', + 'required' => false, + 'default' => [[0.0, 0.0], [1.0, 1.0]] + ]); + + $this->assertEquals(202, $line['headers']['status-code']); + + // Test polygon with default value + $poly = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/polygon', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'area', + 'required' => false, + 'default' => [[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]]] + ]); + + $this->assertEquals(202, $poly['headers']['status-code']); + + // Wait for columns to be available + sleep(2); + + // Create a new row without spatial data to test default values + $newRow = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'rowId' => ID::unique(), + 'data' => [ + 'description' => 'test default values' + ], + 'permissions' => [ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + Permission::delete(Role::user($this->getUser()['$id'])), + ] + ]); + $this->assertEquals(201, $newRow['headers']['status-code']); + + $newRowId = $newRow['body']['$id']; + + // Fetch the row to verify default values are applied + $fetchedRow = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $newRowId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $fetchedRow['headers']['status-code']); + + // Verify default values are applied + $this->assertEquals([0.0, 0.0], $fetchedRow['body']['loc']); + $this->assertEquals([[0.0, 0.0], [1.0, 1.0]], $fetchedRow['body']['route']); + $this->assertEquals([[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]]], $fetchedRow['body']['area']); + } + }