diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 8cfeb5da3b..8bc383cabd 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -225,6 +225,8 @@ App::get('/v1/storage/buckets') $total = $dbForProject->count('buckets', $filterQueries, APP_LIMIT_COUNT); } catch (OrderException $e) { throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } $response->dynamic(new Document([ 'buckets' => $buckets, @@ -853,6 +855,8 @@ App::get('/v1/storage/buckets/:bucketId/files') throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } catch (OrderException $e) { throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } $response->dynamic(new Document([ diff --git a/app/init/database/filters.php b/app/init/database/filters.php index de967b7ab0..90389166c3 100644 --- a/app/init/database/filters.php +++ b/app/init/database/filters.php @@ -255,6 +255,8 @@ Database::addFilter( ->find('variables', [ Query::equal('resourceInternalId', [$document->getSequence()]), Query::equal('resourceType', $resourceType), + Query::orderAsc('resourceType'), + Query::orderAsc(), Query::limit(APP_LIMIT_SUBQUERY), ]); } 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/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php index fc4e2a8a91..3467a9d11c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php @@ -124,6 +124,8 @@ class Delete extends Action throw new Exception($this->getConflictException()); } catch (RestrictedException) { throw new Exception($this->getRestrictedException()); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } foreach ($documents as $document) { 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..65bd255d32 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,9 @@ 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()); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $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 253d7dab3b..03e4450f68 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 e9297e3ec9..c7de7caf7c 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 @@ -382,7 +382,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 9aa138b31d..23fbf205dc 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/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php index 9c8405cf18..546cbeddd4 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php @@ -158,43 +158,6 @@ class XList extends Action ->addMetric(METRIC_DATABASES_OPERATIONS_READS, max($operations, 1)) ->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations); - // Check if the SELECT query includes the removable attributes - $hasWildcard = false; - $hasSelectQueries = !empty($selectQueries); - $requestedAttributes = []; - - if ($hasSelectQueries) { - foreach ($selectQueries as $query) { - if ($query->getMethod() !== Query::TYPE_SELECT) { - continue; - } - - $values = $query->getValues(); - if (\in_array('*', $values, true)) { - $hasWildcard = true; - break; - } - - // Check which removable attributes are explicitly requested - foreach ($this->removableAttributes['*'] as $attribute) { - if (\in_array($attribute, $values, true)) { - $requestedAttributes[$attribute] = true; - } - } - } - - if (!$hasWildcard) { - foreach ($documents as $document) { - // Remove attributes that are not explicitly requested - foreach ($this->removableAttributes['*'] as $attribute) { - if (!isset($requestedAttributes[$attribute])) { - $document->removeAttribute($attribute); - } - } - } - } - } - $response->dynamic(new Document([ 'total' => $total, // rows or documents diff --git a/src/Appwrite/Platform/Workers/StatsResources.php b/src/Appwrite/Platform/Workers/StatsResources.php index 1cd866e9a9..5ec306c5bb 100644 --- a/src/Appwrite/Platform/Workers/StatsResources.php +++ b/src/Appwrite/Platform/Workers/StatsResources.php @@ -434,18 +434,43 @@ class StatsResources extends Action { $message = 'Stats writeDocuments project: ' . $project->getId() . '(' . $project->getSequence() . ')'; + /** + * sort by unique index key reduce locks/deadlocks + */ + usort($this->documents, function ($a, $b) { + // Metric DESC + $cmp = strcmp($b['metric'], $a['metric']); + if ($cmp !== 0) { + return $cmp; + } + + // Period ASC + $cmp = strcmp($a['period'], $b['period']); + if ($cmp !== 0) { + return $cmp; + } + + // Time ASC, NULLs first + if ($a['time'] === null) { + return ($b['time'] === null) ? 0 : -1; + } + if ($b['time'] === null) { + return 1; + } + + return strcmp($a['time'], $b['time']); + }); + try { $dbForLogs->upsertDocuments( 'stats', - $this->documents + $this->documents, ); Console::success($message . ' | Documents: ' . count($this->documents)); } catch (\Throwable $e) { Console::error('Error: ' . $message . ' | Exception: ' . $e->getMessage()); throw $e; - } finally { - $this->documents = []; } } } diff --git a/src/Appwrite/Platform/Workers/StatsUsage.php b/src/Appwrite/Platform/Workers/StatsUsage.php index 3a615072df..bd34d53b00 100644 --- a/src/Appwrite/Platform/Workers/StatsUsage.php +++ b/src/Appwrite/Platform/Workers/StatsUsage.php @@ -424,12 +424,40 @@ class StatsUsage extends Action try { $dbForProject = $getProjectDB($projectStats['project']); Console::log('Processing batch with ' . count($projectStats['stats']) . ' stats'); - $dbForProject->upsertDocumentsWithIncrease('stats', 'value', $projectStats['stats']); - Console::success('Batch successfully written to DB'); - unset($this->projects[$sequence]); + /** + * Sort by unique index key reduce locks/deadlocks + */ + usort($projectStats['stats'], function ($a, $b) { + // Metric DESC + $cmp = strcmp($b['metric'], $a['metric']); + if ($cmp !== 0) { + return $cmp; + } + + // Period ASC + $cmp = strcmp($a['period'], $b['period']); + if ($cmp !== 0) { + return $cmp; + } + + // Time ASC, NULLs first + if ($a['time'] === null) { + return ($b['time'] === null) ? 0 : -1; + } + if ($b['time'] === null) { + return 1; + } + + return strcmp($a['time'], $b['time']); + }); + + $dbForProject->createOrUpdateDocumentsWithIncrease('stats', 'value', $projectStats['stats']); + Console::success('Batch successfully written to DB'); } catch (Throwable $e) { Console::error('Error processing stats: ' . $e->getMessage()); + } finally { + unset($this->projects[$sequence]); } } @@ -468,12 +496,53 @@ class StatsUsage extends Action try { Console::log('Processing batch with ' . count($this->statDocuments) . ' stats'); - $dbForLogs->upsertDocumentsWithIncrease( + + /** + * Sort by UNIQUE KEY "_key_metric_period_time" ("_tenant","metric" DESC,"period","time") + * Here we sort by _tenant as well because of setTenantPerDocument + */ + + usort($this->statDocuments, function ($a, $b) { + // Tenant ASC + $cmp = $a['$tenant'] <=> $b['$tenant']; + if ($cmp !== 0) { + return $cmp; + } + + // Metric DESC + $cmp = strcmp($b['metric'], $a['metric']); + if ($cmp !== 0) { + return $cmp; + } + + // Period ASC + $cmp = strcmp($a['period'], $b['period']); + if ($cmp !== 0) { + return $cmp; + } + + // Time ASC, NULLs first + if ($a['time'] === null) { + return ($b['time'] === null) ? 0 : -1; + } + if ($b['time'] === null) { + return 1; + } + + return strcmp($a['time'], $b['time']); + }); + + $dbForLogs->createOrUpdateDocumentsWithIncrease( 'stats', 'value', $this->statDocuments ); Console::success('Usage logs pushed to Logs DB'); + + /** + * todo: Do we need to unset $this->statDocuments? + */ + } catch (Throwable $th) { Console::error($th->getMessage()); } diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php index c98cb83444..0dd82937a5 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php @@ -5299,8 +5299,8 @@ trait DatabasesBase $this->assertEquals(2, count($response['body']['documents'])); $this->assertEquals(null, $response['body']['documents'][0]['fullName']); $this->assertArrayNotHasKey("libraries", $response['body']['documents'][0]); - $this->assertArrayNotHasKey('$databaseId', $response['body']['documents'][0]); - $this->assertArrayNotHasKey('$collectionId', $response['body']['documents'][0]); + $this->assertArrayHasKey('$databaseId', $response['body']['documents'][0]); + $this->assertArrayHasKey('$collectionId', $response['body']['documents'][0]); } /** @@ -5320,8 +5320,8 @@ trait DatabasesBase $this->assertEquals(200, $response['headers']['status-code']); $this->assertArrayNotHasKey('libraries', $response['body']['documents'][0]); - $this->assertArrayNotHasKey('$databaseId', $response['body']['documents'][0]); - $this->assertArrayNotHasKey('$collectionId', $response['body']['documents'][0]); + $this->assertArrayHasKey('$databaseId', $response['body']['documents'][0]); + $this->assertArrayHasKey('$collectionId', $response['body']['documents'][0]); $response = $this->client->call(Client::METHOD_GET, '/databases/' . $data['databaseId'] . '/collections/' . $data['personCollection'] . '/documents', array_merge([ 'content-type' => 'application/json', @@ -5334,8 +5334,8 @@ trait DatabasesBase $document = $response['body']['documents'][0]; $this->assertEquals(200, $response['headers']['status-code']); $this->assertArrayHasKey('libraries', $document); - $this->assertArrayNotHasKey('$databaseId', $document); - $this->assertArrayNotHasKey('$collectionId', $document); + $this->assertArrayHasKey('$databaseId', $document); + $this->assertArrayHasKey('$collectionId', $document); $response = $this->client->call(Client::METHOD_GET, '/databases/' . $data['databaseId'] . '/collections/' . $data['personCollection'] . '/documents/' . $document['$id'], array_merge([ 'content-type' => 'application/json', @@ -7884,4 +7884,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 cfd1d16a23..1f1de45c2f 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -3085,7 +3085,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'] @@ -3758,7 +3758,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'] @@ -3951,7 +3951,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'] @@ -4307,7 +4307,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'] @@ -5240,8 +5240,8 @@ trait DatabasesBase $this->assertEquals(2, count($response['body']['rows'])); $this->assertEquals(null, $response['body']['rows'][0]['fullName']); $this->assertArrayNotHasKey("libraries", $response['body']['rows'][0]); - $this->assertArrayNotHasKey('$databaseId', $response['body']['rows'][0]); - $this->assertArrayNotHasKey('$tableId', $response['body']['rows'][0]); + $this->assertArrayHasKey('$databaseId', $response['body']['rows'][0]); + $this->assertArrayHasKey('$tableId', $response['body']['rows'][0]); } /** @@ -5261,8 +5261,8 @@ trait DatabasesBase $this->assertEquals(200, $response['headers']['status-code']); $this->assertArrayNotHasKey('libraries', $response['body']['rows'][0]); - $this->assertArrayNotHasKey('$databaseId', $response['body']['rows'][0]); - $this->assertArrayNotHasKey('$tableId', $response['body']['rows'][0]); + $this->assertArrayHasKey('$databaseId', $response['body']['rows'][0]); + $this->assertArrayHasKey('$tableId', $response['body']['rows'][0]); $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $data['databaseId'] . '/tables/' . $data['personCollection'] . '/rows', array_merge([ 'content-type' => 'application/json', @@ -5275,8 +5275,8 @@ trait DatabasesBase $row = $response['body']['rows'][0]; $this->assertEquals(200, $response['headers']['status-code']); $this->assertArrayHasKey('libraries', $row); - $this->assertArrayNotHasKey('$databaseId', $row); - $this->assertArrayNotHasKey('$tableId', $row); + $this->assertArrayHasKey('$databaseId', $row); + $this->assertArrayHasKey('$tableId', $row); $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $data['databaseId'] . '/tables/' . $data['personCollection'] . '/rows/' . $row['$id'], array_merge([ 'content-type' => 'application/json', @@ -8916,4 +8916,279 @@ trait DatabasesBase $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}", $headers); } + public function testSpatialColCreateOnExistingData(): void + { + + if($this->isMongoDB()) { + $this->markTestSkipped('MongoDB is not supported for this test'); + } + + $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 + { + + if($this->isMongoDB()) { + $this->markTestSkipped('MongoDB is not supported for this test'); + } + + $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']); + } + }