Merge branch '1.8.x' of github.com:appwrite/appwrite into feat-mongodb-pulls

# Conflicts:
#	composer.lock
#	src/Appwrite/Platform/Workers/StatsUsage.php
#	tests/e2e/Services/Databases/TablesDB/DatabasesBase.php
This commit is contained in:
shimon
2025-09-21 15:55:11 +03:00
15 changed files with 690 additions and 69 deletions
+4
View File
@@ -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([
+2
View File
@@ -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),
]);
}
@@ -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']) {
@@ -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
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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
@@ -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 = [];
@@ -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 = [];
@@ -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
@@ -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 = [];
}
}
}
+73 -4
View File
@@ -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());
}
@@ -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']);
}
}
@@ -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']);
}
}