Support legacy

This commit is contained in:
Jake Barnby
2025-09-09 00:50:48 +12:00
parent 7c3ba74710
commit 495eeeb929
31 changed files with 4670 additions and 51 deletions
-1
View File
@@ -169,7 +169,6 @@ jobs:
Console,
Databases/Legacy,
Databases/TablesDB,
Databases/Transactions,
Functions,
FunctionsSchedule,
GraphQL,
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Transactions;
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Transactions;
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Transactions;
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Transactions\Operations;
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Operations;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
@@ -53,7 +53,7 @@ class Create extends Action
contentType: ContentType::JSON
))
->param('transactionId', '', new UID(), 'Transaction ID.')
->param('operations', [], new ArrayList(new Operation()), 'Array of staged operations.', true)
->param('operations', [], new ArrayList(new Operation(type: 'legacy')), 'Array of staged operations.', true)
->inject('response')
->inject('dbForProject')
->inject('plan')
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Transactions;
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
use Appwrite\Event\Delete;
use Appwrite\Extend\Exception;
@@ -73,7 +73,7 @@ class Update extends Action
* @param bool $rollback
* @param UtopiaResponse $response
* @param Database $dbForProject
* @param Delete $queueForDeletes
* @param \Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Delete $queueForDeletes
* @return void
* @throws ConflictException
* @throws Exception
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Transactions;
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
@@ -0,0 +1,54 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions;
use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Create as TransactionsCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Range;
class Create extends TransactionsCreate
{
public static function getName(): string
{
return 'createTransaction';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_TRANSACTION;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/tablesdb/transactions')
->desc('Create transaction')
->groups(['api', 'database', 'transactions'])
->label('scope', 'transactions.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'tablesDB',
group: 'transactions',
name: 'createTransaction',
description: '/docs/references/tablesdb/create-transaction.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: UtopiaResponse::MODEL_TRANSACTION,
)
],
contentType: ContentType::JSON
))
->param('ttl', APP_DATABASE_TXN_TTL_DEFAULT, new Range(min: APP_DATABASE_TXN_TTL_MIN, max: APP_DATABASE_TXN_TTL_MAX), 'Seconds before the transaction expires.', true)
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
}
@@ -0,0 +1,54 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions;
use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Delete as TransactionsDelete;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Delete extends TransactionsDelete
{
public static function getName(): string
{
return 'deleteTransaction';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_NONE;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/tablesdb/transactions/:transactionId')
->desc('Delete transaction')
->groups(['api', 'database', 'transactions'])
->label('scope', 'transactions.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'tablesDB',
group: 'transactions',
name: 'deleteTransaction',
description: '/docs/references/tablesdb/delete-transaction.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_NOCONTENT,
model: UtopiaResponse::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('transactionId', '', new UID(), 'Transaction ID.')
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
}
@@ -0,0 +1,54 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions;
use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Get as TransactionsGet;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Get extends TransactionsGet
{
public static function getName(): string
{
return 'getTransaction';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_TRANSACTION;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/tablesdb/transactions/:transactionId')
->desc('Get transaction')
->groups(['api', 'database', 'transactions'])
->label('scope', 'transactions.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'tablesDB',
group: 'transactions',
name: 'getTransaction',
description: '/docs/references/tablesdb/get-transaction.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: UtopiaResponse::MODEL_TRANSACTION,
)
],
contentType: ContentType::JSON
))
->param('transactionId', '', new UID(), 'Transaction ID.')
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
}
@@ -0,0 +1,58 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions\Operations;
use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Operations\Create as OperationsCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Operation;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
class Create extends OperationsCreate
{
public static function getName(): string
{
return 'createOperations';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_TRANSACTION;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/tablesdb/transactions/:transactionId/operations')
->desc('Add operations to transaction')
->groups(['api', 'database', 'transactions'])
->label('scope', 'transactions.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'tablesDB',
group: 'transactions',
name: 'createOperations',
description: '/docs/references/tablesdb/create-operations.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: UtopiaResponse::MODEL_TRANSACTION,
)
],
contentType: ContentType::JSON
))
->param('transactionId', '', new UID(), 'Transaction ID.')
->param('operations', [], new ArrayList(new Operation(type: 'tablesdb')), 'Array of staged operations.', true)
->inject('response')
->inject('dbForProject')
->inject('plan')
->callback($this->action(...));
}
}
@@ -0,0 +1,58 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions;
use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Update as TransactionsUpdate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
class Update extends TransactionsUpdate
{
public static function getName(): string
{
return 'updateTransaction';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_TRANSACTION;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/tablesdb/transactions/:transactionId')
->desc('Update transaction')
->groups(['api', 'database', 'transactions'])
->label('scope', 'transactions.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'tablesDB',
group: 'transactions',
name: 'updateTransaction',
description: '/docs/references/tablesdb/update-transaction.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: UtopiaResponse::MODEL_TRANSACTION,
)
],
contentType: ContentType::JSON
))
->param('transactionId', '', new UID(), 'Transaction ID.')
->param('commit', false, new Boolean(), 'Commit transaction?', true)
->param('rollback', false, new Boolean(), 'Rollback transaction?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDeletes')
->callback($this->action(...));
}
}
@@ -0,0 +1,54 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions;
use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\XList as TransactionsList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Transactions;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Swoole\Response as SwooleResponse;
class XList extends TransactionsList
{
public static function getName(): string
{
return 'listTransactions';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_TRANSACTION_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/tablesdb/transactions')
->desc('List transactions')
->groups(['api', 'database', 'transactions'])
->label('scope', 'transactions.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'tablesDB',
group: 'transactions',
name: 'listTransactions',
description: '/docs/references/tablesdb/list-transactions.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: UtopiaResponse::MODEL_TRANSACTION_LIST,
)
],
contentType: ContentType::JSON
))
->param('queries', [], new Transactions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries).', true)
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
}
@@ -19,7 +19,6 @@ class Http extends Service
foreach ([
LegacyRegistry::class,
TablesDBRegistry::class,
TransactionsRegistry::class,
] as $registrar) {
new $registrar($this);
}
@@ -58,6 +58,12 @@ use Appwrite\Platform\Modules\Databases\Http\Databases\Create as CreateDatabase;
use Appwrite\Platform\Modules\Databases\Http\Databases\Delete as DeleteDatabase;
use Appwrite\Platform\Modules\Databases\Http\Databases\Get as GetDatabase;
use Appwrite\Platform\Modules\Databases\Http\Databases\Logs\XList as ListDatabaseLogs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Create as CreateTransaction;
use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Delete as DeleteTransaction;
use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Get as GetTransaction;
use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Operations\Create as CreateOperations;
use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Update as UpdateTransaction;
use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\XList as ListTransactions;
use Appwrite\Platform\Modules\Databases\Http\Databases\Update as UpdateDatabase;
use Appwrite\Platform\Modules\Databases\Http\Databases\Usage\Get as GetDatabaseUsage;
use Appwrite\Platform\Modules\Databases\Http\Databases\Usage\XList as ListDatabaseUsage;
@@ -82,6 +88,7 @@ class Legacy extends Base
$this->registerDocumentActions($service);
$this->registerAttributeActions($service);
$this->registerIndexActions($service);
$this->registerTransactionActions($service);
}
public function registerDatabaseActions(Service $service): void
@@ -191,4 +198,14 @@ class Legacy extends Base
$service->addAction(DeleteIndex::getName(), new DeleteIndex());
$service->addAction(ListIndexes::getName(), new ListIndexes());
}
private function registerTransactionActions(Service $service): void
{
$service->addAction(CreateTransaction::getName(), new CreateTransaction());
$service->addAction(GetTransaction::getName(), new GetTransaction());
$service->addAction(UpdateTransaction::getName(), new UpdateTransaction());
$service->addAction(DeleteTransaction::getName(), new DeleteTransaction());
$service->addAction(ListTransactions::getName(), new ListTransactions());
$service->addAction(CreateOperations::getName(), new CreateOperations());
}
}
@@ -57,6 +57,12 @@ use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Rows\XList as ListR
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Update as UpdateTable;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Usage\Get as GetTableUsage;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\XList as ListTables;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions\Create as CreateTransaction;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions\Delete as DeleteTransaction;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions\Get as GetTransaction;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions\Operations\Create as CreateOperations;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions\Update as UpdateTransaction;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Transactions\XList as ListTransactions;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Update as UpdateTablesDatabase;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Usage\Get as GetTablesDatabaseUsage;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Usage\XList as ListTablesDatabaseUsage;
@@ -81,6 +87,7 @@ class TablesDB extends Base
$this->registerColumnActions($service);
$this->registerIndexActions($service);
$this->registerRowActions($service);
$this->registerTransactionActions($service);
}
private function registerDatabaseActions(Service $service): void
@@ -188,4 +195,14 @@ class TablesDB extends Base
$service->addAction(IncrementRowColumn::getName(), new IncrementRowColumn());
$service->addAction(DecrementRowColumn::getName(), new DecrementRowColumn());
}
private function registerTransactionActions(Service $service): void
{
$service->addAction(CreateTransaction::getName(), new CreateTransaction());
$service->addAction(GetTransaction::getName(), new GetTransaction());
$service->addAction(UpdateTransaction::getName(), new UpdateTransaction());
$service->addAction(DeleteTransaction::getName(), new DeleteTransaction());
$service->addAction(ListTransactions::getName(), new ListTransactions());
$service->addAction(CreateOperations::getName(), new CreateOperations());
}
}
@@ -1,27 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Services\Registry;
use Appwrite\Platform\Modules\Databases\Http\Transactions\Create as CreateTransaction;
use Appwrite\Platform\Modules\Databases\Http\Transactions\Delete as DeleteTransaction;
use Appwrite\Platform\Modules\Databases\Http\Transactions\Get as GetTransaction;
use Appwrite\Platform\Modules\Databases\Http\Transactions\Operations\Create as CreateOperations;
use Appwrite\Platform\Modules\Databases\Http\Transactions\Update as UpdateTransaction;
use Appwrite\Platform\Modules\Databases\Http\Transactions\XList as ListTransactions;
use Utopia\Platform\Service;
/**
* Registers all HTTP actions related to transactions in the module.
*/
class Transactions extends Base
{
public function register(Service $service): void
{
$service->addAction(CreateTransaction::getName(), new CreateTransaction());
$service->addAction(GetTransaction::getName(), new GetTransaction());
$service->addAction(UpdateTransaction::getName(), new UpdateTransaction());
$service->addAction(DeleteTransaction::getName(), new DeleteTransaction());
$service->addAction(ListTransactions::getName(), new ListTransactions());
$service->addAction(CreateOperations::getName(), new CreateOperations());
}
}
@@ -11,7 +11,6 @@ class Operation extends Validator
/** @var array<string> */
private array $required = [
'databaseId',
'collectionId',
'action',
];
@@ -39,6 +38,27 @@ class Operation extends Validator
'bulkDelete' => true,
];
private string $collectionIdName = '';
private string $documentIdName = '';
public function __construct(private readonly string $type)
{
switch ($this->type) {
case 'legacy':
$this->collectionIdName = 'collectionId';
$this->documentIdName = 'documentId';
break;
case 'tablesdb':
$this->collectionIdName = 'tableId';
$this->documentIdName = 'rowId';
break;
default:
throw new \InvalidArgumentException('Invalid type provided.');
}
$this->required[] = $this->collectionIdName;
}
public function getDescription(): string
{
return $this->description;
@@ -85,9 +105,9 @@ class Operation extends Validator
// If action requires documentId, it must be present
if (
isset($this->requiresDocumentId[$value['action']]) &&
!\array_key_exists('documentId', $value)
!\array_key_exists($this->documentIdName, $value)
) {
$this->description = "Key 'documentId' is required for action '{$value['action']}'";
$this->description = "Key '$this->documentIdName' is required for action '{$value['action']}'";
return false;
}
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases\Legacy;
namespace Tests\E2E\Services\Databases\Legacy\Permissions;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases\Legacy;
namespace Tests\E2E\Services\Databases\Legacy\Permissions;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases\TablesDB;
namespace Tests\E2E\Services\Databases\Legacy\Permissions;
use Tests\E2E\Client;
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases\Legacy;
namespace Tests\E2E\Services\Databases\Legacy\Permissions;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -1,13 +1,12 @@
<?php
namespace Tests\E2E\Services\Databases\Transactions;
namespace Tests\E2E\Services\Databases\Legacy\Transactions;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases\Transactions;
namespace Tests\E2E\Services\Databases\Legacy\Transactions;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases\TablesDB;
namespace Tests\E2E\Services\Databases\TablesDB\Permissions;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases\TablesDB;
namespace Tests\E2E\Services\Databases\TablesDB\Permissions;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases\Legacy;
namespace Tests\E2E\Services\Databases\TablesDB\Permissions;
use Tests\E2E\Client;
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases\TablesDB;
namespace Tests\E2E\Services\Databases\TablesDB\Permissions;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -0,0 +1,625 @@
<?php
namespace Tests\E2E\Services\Databases\Legacy\Transactions;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
class ACIDTest extends Scope
{
use ProjectCustom;
use SideClient;
/**
* Test atomicity - all operations succeed or all fail
*/
public function testAtomicity(): void
{
// Create database
$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']
]), [
'databaseId' => ID::unique(),
'name' => 'AtomicityTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection with unique constraint
$collection = $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']
]), [
'collectionId' => ID::unique(),
'name' => 'AtomicityTest',
'documentSecurity' => false,
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
],
]);
$collectionId = $collection['body']['$id'];
// Add unique column
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'email',
'size' => 256,
'required' => true,
]);
// Add unique index
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'unique_email',
'type' => Database::INDEX_UNIQUE,
'columns' => ['email']
]);
sleep(3);
// Create first document outside transaction
$doc1 = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => ID::unique(),
'data' => [
'email' => 'existing@example.com'
]
]);
$this->assertEquals(201, $doc1['headers']['status-code']);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(201, $transaction['headers']['status-code'], 'Transaction creation should succeed. Response: ' . json_encode($transaction));
$this->assertArrayHasKey('$id', $transaction['body'], 'Transaction response should have $id. Response body: ' . json_encode($transaction['body']));
$transactionId = $transaction['body']['$id'];
// Add operations - second one will fail due to unique constraint
$response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'operations' => [
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'action' => 'create',
'documentId' => ID::unique(),
'data' => [
'email' => 'newuser@example.com' // This should succeed
]
],
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'action' => 'create',
'documentId' => ID::unique(),
'data' => [
'email' => 'existing@example.com' // This will fail - duplicate
]
],
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'action' => 'create',
'documentId' => ID::unique(),
'data' => [
'email' => 'anotheruser@example.com' // This should not be created due to atomicity
]
]
]
]);
$this->assertEquals(201, $response['headers']['status-code'], 'Add operations failed. Response: ' . json_encode($response['body']));
// Attempt to commit - should fail due to unique constraint violation
$response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
if ($response['headers']['status-code'] === 200) {
// If transaction succeeded, all documents should be created
$documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
// Should have 4 documents total (1 original + 3 from transaction)
// But since we have a unique constraint violation, this might fail
$this->assertGreaterThanOrEqual(1, $documents['body']['total']);
} else {
$this->assertEquals(409, $response['headers']['status-code']); // Conflict error
// Verify NO new documents were created (atomicity)
$documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(1, $documents['body']['total']); // Only the original document
$this->assertEquals('existing@example.com', $documents['body']['documents'][0]['email']);
}
}
/**
* Test consistency - schema validation and constraints
*/
public function testConsistency(): void
{
// Create database
$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']
]), [
'databaseId' => ID::unique(),
'name' => 'ConsistencyTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection with required fields and constraints
$collection = $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']
]), [
'collectionId' => ID::unique(),
'name' => 'ConsistencyTest',
'documentSecurity' => false,
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
],
]);
$collectionId = $collection['body']['$id'];
// Add required string column
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'required_field',
'size' => 256,
'required' => true,
]);
// Add integer column with min/max constraints
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/integer', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'age',
'required' => true,
'min' => 18,
'max' => 100
]);
sleep(3);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$transactionId = $transaction['body']['$id'];
// Add operations with both valid and invalid data
$response = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'operations' => [
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'action' => 'create',
'documentId' => ID::unique(),
'data' => [
'required_field' => 'Valid User',
'age' => 25 // Valid age
]
],
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'action' => 'create',
'documentId' => ID::unique(),
'data' => [
'required_field' => 'Too Young User',
'age' => 10 // Below minimum - will fail constraint
]
],
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'action' => 'create',
'documentId' => ID::unique(),
'data' => [
'required_field' => 'Another Valid User',
'age' => 30 // Valid but should not be created due to transaction failure
]
]
]
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Attempt to commit - should fail due to constraint violation
$response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
$this->assertContains($response['headers']['status-code'], [400, 500], 'Transaction commit should fail due to validation. Response: ' . json_encode($response['body']));
// Verify no documents were created
$documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(0, $documents['body']['total']);
}
/**
* Test isolation - concurrent transactions on same data
*/
public function testIsolation(): void
{
// Create database
$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']
]), [
'databaseId' => ID::unique(),
'name' => 'IsolationTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection
$collection = $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']
]), [
'collectionId' => ID::unique(),
'name' => 'IsolationTest',
'documentSecurity' => false,
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
],
]);
$collectionId = $collection['body']['$id'];
// Add counter column
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/integer', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'counter',
'required' => true,
'min' => 0,
'max' => 1000000
]);
sleep(2);
// Create initial document with counter
$doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'shared_counter',
'data' => [
'counter' => 0
]
]);
$this->assertEquals(201, $doc['headers']['status-code']);
// Create first transaction
$transaction1 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(201, $transaction1['headers']['status-code'], 'Transaction 1 creation should succeed');
$this->assertArrayHasKey('$id', $transaction1['body'], 'Transaction 1 response should have $id');
$transactionId1 = $transaction1['body']['$id'];
// Create second transaction
$transaction2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(201, $transaction2['headers']['status-code'], 'Transaction 2 creation should succeed');
$this->assertArrayHasKey('$id', $transaction2['body'], 'Transaction 2 response should have $id');
$transactionId2 = $transaction2['body']['$id'];
// Transaction 1: Increment counter by 10
$this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId1}/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'operations' => [
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'documentId' => 'shared_counter',
'action' => 'increment',
'data' => [
'column' => 'counter',
'value' => 10
]
]
]
]);
// Transaction 2: Increment counter by 5
$this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId2}/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'operations' => [
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'documentId' => 'shared_counter',
'action' => 'increment',
'data' => [
'column' => 'counter',
'value' => 5
]
]
]
]);
// Commit first transaction
$response1 = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId1}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
$this->assertEquals(200, $response1['headers']['status-code']);
// Commit second transaction
$response2 = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId2}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
$this->assertEquals(200, $response2['headers']['status-code']);
// Check final value - both increments should be applied
$document = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/shared_counter", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
// Both increments should be applied: 0 + 10 + 5 = 15
$this->assertEquals(15, $document['body']['counter']);
}
/**
* Test durability - committed data persists
*/
public function testDurability(): void
{
// Create database
$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']
]), [
'databaseId' => ID::unique(),
'name' => 'DurabilityTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection
$collection = $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']
]), [
'collectionId' => ID::unique(),
'name' => 'DurabilityTest',
'documentSecurity' => false,
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$collectionId = $collection['body']['$id'];
// Add column
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $collectionId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'data',
'size' => 256,
'required' => true,
]);
sleep(2);
// Create and commit transaction with multiple operations
$transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(201, $transaction['headers']['status-code'], 'Transaction creation should succeed');
$this->assertArrayHasKey('$id', $transaction['body'], 'Transaction response should have $id');
$transactionId = $transaction['body']['$id'];
// Add multiple operations
$this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'operations' => [
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'action' => 'create',
'documentId' => 'durable_doc_1',
'data' => [
'data' => 'Important data 1'
]
],
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'action' => 'create',
'documentId' => 'durable_doc_2',
'data' => [
'data' => 'Important data 2'
]
],
[
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'action' => 'update',
'documentId' => 'durable_doc_1',
'data' => [
'data' => 'Updated important data 1'
]
]
]
]);
// Commit transaction
$response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'commit' => true
]);
$this->assertEquals(200, $response['headers']['status-code'], 'Commit should succeed. Response: ' . json_encode($response['body']));
$this->assertEquals('committed', $response['body']['status']);
// List all documents to see what was created
$allDocs = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertGreaterThan(0, $allDocs['body']['total'], 'Should have created documents. Found: ' . json_encode($allDocs['body']));
// Verify documents exist and have correct data
$document1 = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/durable_doc_1", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $document1['headers']['status-code']);
$this->assertEquals('Updated important data 1', $document1['body']['data']);
$document2 = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/durable_doc_2", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $document2['headers']['status-code']);
$this->assertEquals('Important data 2', $document2['body']['data']);
// Further update outside transaction to ensure persistence
$update = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/durable_doc_1", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'data' => 'Modified outside transaction'
]
]);
$this->assertEquals(200, $update['headers']['status-code']);
// Verify the update persisted
$document1 = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows/durable_doc_1", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals('Modified outside transaction', $document1['body']['data']);
// List all documents to verify total count
$documents = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$collectionId}/rows", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(2, $documents['body']['total']);
}
}
File diff suppressed because it is too large Load Diff