Merge branch '1.8.x' into cursor/add-type-enum-support-and-update-docs-3769

This commit is contained in:
Chirag Aggarwal 2025-09-22 15:52:43 +07:00 committed by GitHub
commit d5d6f9a748
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 786 additions and 150 deletions

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([

View file

@ -10,6 +10,7 @@ use Swoole\Http\Response as SwooleResponse;
use Swoole\Http\Server;
use Swoole\Process;
use Swoole\Table;
use Swoole\Timer;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
@ -156,11 +157,16 @@ $http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) {
Console::success('Worker ' . ++$workerId . ' started successfully');
});
$http->on(Constant::EVENT_BEFORE_RELOAD, function ($server, $workerId) {
$http->on(Constant::EVENT_WORKER_STOP, function ($server, $workerId) {
Timer::clearAll();
Console::success('Worker ' . ++$workerId . ' stopped successfully');
});
$http->on(Constant::EVENT_BEFORE_RELOAD, function ($server) {
Console::success('Starting reload...');
});
$http->on(Constant::EVENT_AFTER_RELOAD, function ($server, $workerId) {
$http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
Console::success('Reload completed...');
});
@ -550,7 +556,7 @@ $http->on(Constant::EVENT_TASK, function () use ($register, $domains) {
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
Console::loop(function () use ($dbForPlatform, $domains, &$lastSyncUpdate) {
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $domains, &$lastSyncUpdate) {
try {
$time = DateTime::now();
$limit = 1000;
@ -589,8 +595,6 @@ $http->on(Constant::EVENT_TASK, function () use ($register, $domains) {
} catch (Throwable $th) {
Console::error($th->getMessage());
}
}, DOMAIN_SYNC_TIMER, 0, function ($error) {
Console::error($error);
});
});

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),
]);
}

82
composer.lock generated
View file

@ -756,24 +756,21 @@
},
{
"name": "google/protobuf",
"version": "v4.32.0",
"version": "v4.32.1",
"source": {
"type": "git",
"url": "https://github.com/protocolbuffers/protobuf-php.git",
"reference": "9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646"
"reference": "c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646",
"reference": "9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646",
"url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb",
"reference": "c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb",
"shasum": ""
},
"require": {
"php": ">=8.1.0"
},
"provide": {
"ext-protobuf": "*"
},
"require-dev": {
"phpunit/phpunit": ">=5.0.0 <8.5.27"
},
@ -797,9 +794,9 @@
"proto"
],
"support": {
"source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.32.0"
"source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.32.1"
},
"time": "2025-08-14T20:00:33+00:00"
"time": "2025-09-14T05:14:52+00:00"
},
{
"name": "league/csv",
@ -1355,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": {
@ -1414,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",
@ -3638,16 +3635,16 @@
},
{
"name": "utopia-php/database",
"version": "1.4.8",
"version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "dbecdf89fde33a5f81ec19f4f97fe0c3715dc83a"
"reference": "24c4519b4ac32aee13af31dddd984db2a3b34980"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/dbecdf89fde33a5f81ec19f4f97fe0c3715dc83a",
"reference": "dbecdf89fde33a5f81ec19f4f97fe0c3715dc83a",
"url": "https://api.github.com/repos/utopia-php/database/zipball/24c4519b4ac32aee13af31dddd984db2a3b34980",
"reference": "24c4519b4ac32aee13af31dddd984db2a3b34980",
"shasum": ""
},
"require": {
@ -3688,9 +3685,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/1.4.8"
"source": "https://github.com/utopia-php/database/tree/1.5.0"
},
"time": "2025-09-12T03:35:59+00:00"
"time": "2025-09-18T14:42:01+00:00"
},
{
"name": "utopia-php/detector",
@ -5007,16 +5004,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.3.4",
"version": "1.3.5",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "d3b420dced42f1eec1f6d0aa98b7bbf8de4042ac"
"reference": "6fda9e58b37c9872c1a2a424e5467de8de1bc567"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/d3b420dced42f1eec1f6d0aa98b7bbf8de4042ac",
"reference": "d3b420dced42f1eec1f6d0aa98b7bbf8de4042ac",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6fda9e58b37c9872c1a2a424e5467de8de1bc567",
"reference": "6fda9e58b37c9872c1a2a424e5467de8de1bc567",
"shasum": ""
},
"require": {
@ -5052,9 +5049,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/1.3.4"
"source": "https://github.com/appwrite/sdk-generator/tree/1.3.5"
},
"time": "2025-09-08T11:56:04+00:00"
"time": "2025-09-15T04:19:40+00:00"
},
{
"name": "doctrine/annotations",
@ -5281,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": {
@ -5301,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",
@ -5314,9 +5311,6 @@
],
"type": "project",
"autoload": {
"files": [
"overrides/Runner/Parallel/ProcessFactory.php"
],
"psr-4": {
"App\\": "app/",
"Database\\Seeders\\": "database/seeders/",
@ -5346,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",
@ -6236,16 +6230,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.6.26",
"version": "9.6.27",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "a0139ea157533454f611038326f3020b3051f129"
"reference": "0a9aa4440b6a9528cf360071502628d717af3e0a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a0139ea157533454f611038326f3020b3051f129",
"reference": "a0139ea157533454f611038326f3020b3051f129",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0a9aa4440b6a9528cf360071502628d717af3e0a",
"reference": "0a9aa4440b6a9528cf360071502628d717af3e0a",
"shasum": ""
},
"require": {
@ -6319,7 +6313,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.26"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.27"
},
"funding": [
{
@ -6343,7 +6337,7 @@
"type": "tidelift"
}
],
"time": "2025-09-11T06:17:45+00:00"
"time": "2025-09-14T06:18:03+00:00"
},
{
"name": "psr/cache",
@ -8512,7 +8506,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {

View file

@ -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']) {

View file

@ -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

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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

View file

@ -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 = [];

View file

@ -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 = [];

View file

@ -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

View file

@ -77,8 +77,8 @@ class Get extends Action
str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_COLLECTIONS),
str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_DOCUMENTS),
str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_STORAGE),
str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASES_OPERATIONS_READS),
str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASES_OPERATIONS_WRITES)
str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_READS),
str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES)
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {

View file

@ -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->createOrUpdateDocuments(
'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 = [];
}
}
}

View file

@ -424,12 +424,40 @@ class StatsUsage extends Action
try {
$dbForProject = $getProjectDB($projectStats['project']);
Console::log('Processing batch with ' . count($projectStats['stats']) . ' stats');
/**
* 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');
unset($this->projects[$sequence]);
} 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');
/**
* 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());
}

View file

@ -11,8 +11,6 @@ class V16 extends Filter
{
switch ($model) {
case 'functions.create':
$content['commands'] = $this->getCommands($content['runtime'] ?? '');
break;
case 'functions.update':
$content['commands'] = $this->getCommands($content['runtime'] ?? '');
break;

View file

@ -2,6 +2,7 @@
namespace Appwrite\Utopia\Request\Filters;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Request\Filter;
use Utopia\Database\Query;
@ -67,9 +68,9 @@ class V17 extends Filter
foreach ($content['queries'] as $query) {
try {
$query = $this->parseQuery($query);
$parsed[] = json_encode(array_filter($query->toArray()));
$parsed[] = \json_encode(\array_filter($query->toArray()));
} catch (\Throwable $th) {
throw new \Exception("Invalid query: {$query}", previous: $th);
throw new Exception(Exception::GENERAL_QUERY_INVALID, $th->getMessage());
}
}
@ -83,6 +84,7 @@ class V17 extends Filter
{
// Init empty vars we fill later
$method = '';
$attribute = null;
$params = [];
// Separate method from filter
@ -92,7 +94,7 @@ class V17 extends Filter
throw new \Exception('Invalid query');
}
$method = mb_substr($filter, 0, $paramsStart);
$method = \mb_substr($filter, 0, $paramsStart);
// Separate params from filter
$paramsEnd = \strlen($filter) - 1; // -1 to ignore )
@ -103,14 +105,13 @@ class V17 extends Filter
throw new \Exception('Invalid query method');
}
$currentParam = ""; // We build param here before pushing when it's ended
$currentParam = ''; // We build param here before pushing when it's ended
$currentArrayParam = []; // We build array param here before pushing when it's ended
$stack = []; // State for stack of parentheses
$stackCount = 0; // Length of stack array. Kept as variable to improve performance
$stringStackState = null; // State for string support
// Loop thorough all characters
for ($i = $parametersStart; $i < $paramsEnd; $i++) {
$char = $filter[$i];
@ -135,20 +136,25 @@ class V17 extends Filter
($filter[$i - 1] !== static::CHAR_BACKSLASH || $filter[$i - 2] === static::CHAR_BACKSLASH) // Must not be escaped;
) {
if ($isStringStack) {
// Dont mix-up string symbols. Only allow the same as on start
// Don't mix up string symbols. Only allow the same as on start
if ($char === $stringStackState) {
// End of string
$stringStackState = null;
}
// Either way, add symbol to builder
static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam);
} else {
// Start of string
$stringStackState = $char;
static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam);
}
// Either way, add symbol to builder
static::appendSymbol(
$isStringStack,
$char,
$i,
$filter,
$currentParam,
);
continue;
}
@ -174,12 +180,12 @@ class V17 extends Filter
continue;
} elseif ($char === static::CHAR_COMMA) { // Params separation support
// If in array stack, dont merge yet, just mark it in array param builder
// If in array stack, don't merge yet, just mark it in array param builder
if ($isArrayStack) {
$currentArrayParam[] = $currentParam;
$currentParam = "";
} else {
// Append from parap builder. Either value, or array
// Append from param builder. Either value, or array
if (empty($currentArrayParam)) {
if (strlen($currentParam)) {
$params[] = $currentParam;
@ -193,23 +199,28 @@ class V17 extends Filter
}
// Value, not relevant to syntax
static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam);
static::appendSymbol(
$isStringStack,
$char,
$i,
$filter,
$currentParam,
);
}
if (strlen($currentParam)) {
if (\strlen($currentParam)) {
$params[] = $currentParam;
$currentParam = "";
$currentParam = '';
}
$parsedParams = [];
foreach ($params as $param) {
// If array, parse each child separatelly
// If array, parse each child separately
if (\is_array($param)) {
foreach ($param as $element) {
$arr[] = self::parseValue($element);
}
$parsedParams[] = $arr ?? [];
} else {
$parsedParams[] = self::parseValue($param);
@ -295,8 +306,13 @@ class V17 extends Filter
* @param string $currentParam
* @return void
*/
private function appendSymbol(bool $isStringStack, string $char, int $index, string $filter, string &$currentParam): void
{
private function appendSymbol(
bool $isStringStack,
string $char,
int $index,
string $filter,
string &$currentParam
): void {
// Ignore spaces and commas outside of string
$canBeIgnored = false;

View file

@ -2,8 +2,10 @@
namespace Appwrite\Utopia\Request\Filters;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Request\Filter;
use Utopia\Database\Database;
use Utopia\Database\Exception\NotFound;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
@ -54,8 +56,8 @@ class V20 extends Filter
try {
$parsed = Query::parseQueries($content['queries']);
} catch (QueryException) {
return $content;
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$selections = Query::groupByType($parsed)['selections'] ?? [];
@ -136,17 +138,28 @@ class V20 extends Filter
return [];
}
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
return [];
try {
$database = Authorization::skip(fn () => $dbForProject->getDocument(
'databases',
$databaseId
));
if ($database->isEmpty()) {
return [];
}
} catch (NotFound) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument(
'database_' . $database->getSequence(),
$collectionId
));
if ($collection->isEmpty()) {
return [];
try {
$collection = Authorization::skip(fn () => $dbForProject->getDocument(
'database_' . $database->getSequence(),
$collectionId
));
if ($collection->isEmpty()) {
return [];
}
} catch (NotFound) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
$attributes = $collection->getAttribute('attributes', []);

View file

@ -5270,8 +5270,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]);
}
/**
@ -5291,8 +5291,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',
@ -5305,8 +5305,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',
@ -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']);
}
}

View file

@ -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']
@ -5197,8 +5197,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]);
}
/**
@ -5218,8 +5218,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',
@ -5232,8 +5232,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',
@ -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']);
}
}