mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
added transactions for vectordb
This commit is contained in:
@@ -20,6 +20,7 @@ abstract class Action extends UtopiaAction
|
||||
public function setHttpPath(string $path): UtopiaAction
|
||||
{
|
||||
switch (true) {
|
||||
// TODO: set the getDatabaseType() from each database group instead of path matching
|
||||
case str_contains($path, '/tablesdb'):
|
||||
$this->context = TABLES;
|
||||
$this->databaseType = TABLESDB;
|
||||
@@ -29,6 +30,10 @@ abstract class Action extends UtopiaAction
|
||||
$this->context = COLLECTIONS;
|
||||
$this->databaseType = DOCUMENTSDB;
|
||||
break;
|
||||
case str_contains($path, '/vectordb'):
|
||||
$this->context = COLLECTIONS;
|
||||
$this->databaseType = VECTORDB;
|
||||
break;
|
||||
}
|
||||
return parent::setHttpPath($path);
|
||||
}
|
||||
|
||||
@@ -286,6 +286,11 @@ class Update extends Action
|
||||
$data = $data->getArrayCopy();
|
||||
}
|
||||
|
||||
// Decode JSON strings that may have been encoded when storing nested arrays/objects
|
||||
if (\is_array($data)) {
|
||||
$data = $this->decodeJsonStrings($data);
|
||||
}
|
||||
|
||||
if (!isset($dbCache[$databaseInternalId])) {
|
||||
$databaseDoc = Authorization::skip(fn () => $dbForProject->findOne('databases', [
|
||||
Query::equal('$sequence', [$databaseInternalId])
|
||||
@@ -710,6 +715,8 @@ class Update extends Action
|
||||
array &$state
|
||||
): int {
|
||||
$count = 0;
|
||||
// Decode JSON strings in bulk create data
|
||||
$data = $this->decodeJsonStrings($data);
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state, &$count) {
|
||||
$documents = \array_map(function ($doc) {
|
||||
return $doc instanceof Document ? $doc : new Document($doc);
|
||||
@@ -748,8 +755,11 @@ class Update extends Action
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): int {
|
||||
// extracting query first then decoding the nested transaction log data
|
||||
// otherwise can result error as queries would get transformed to array instead of string
|
||||
$queries = Query::parseQueries($data['queries'] ?? []);
|
||||
$updateData = new Document($data['data']);
|
||||
$data = $this->decodeJsonStrings($data);
|
||||
|
||||
$dependentDocs = [];
|
||||
|
||||
@@ -820,6 +830,8 @@ class Update extends Action
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): int {
|
||||
// Decode JSON strings in bulk upsert data
|
||||
$data = $this->decodeJsonStrings($data);
|
||||
$documents = \array_map(function ($doc) {
|
||||
return $doc instanceof Document ? $doc : new Document($doc);
|
||||
}, $data);
|
||||
@@ -895,4 +907,31 @@ class Update extends Action
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively decode JSON strings in data array
|
||||
* This handles cases where nested arrays/objects are stored as JSON strings in the database
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
private function decodeJsonStrings(array $data): array
|
||||
{
|
||||
foreach ($data as $key => $value) {
|
||||
if (\is_string($value)) {
|
||||
$decoded = \json_decode($value, true);
|
||||
if (\json_last_error() === JSON_ERROR_NONE && (\is_array($decoded) || \is_object($decoded))) {
|
||||
$data[$key] = $decoded;
|
||||
// Recursively decode nested structures
|
||||
if (\is_array($decoded)) {
|
||||
$data[$key] = $this->decodeJsonStrings($decoded);
|
||||
}
|
||||
}
|
||||
} elseif (\is_array($value)) {
|
||||
// Recursively process nested arrays
|
||||
$data[$key] = $this->decodeJsonStrings($value);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\VectorDB\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 'createVectorDBTransaction';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_TRANSACTION;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/vectordb/transactions')
|
||||
->desc('Create transaction')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'documents.write')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vectorDB',
|
||||
group: 'transactions',
|
||||
name: 'createTransaction',
|
||||
description: '/docs/references/vectordb/create-transaction.md',
|
||||
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
|
||||
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')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\VectorDB\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 'deleteVectorDBTransaction';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_NONE;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
|
||||
->setHttpPath('/v1/vectordb/transactions/:transactionId')
|
||||
->desc('Delete transaction')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'documents.write')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vectorDB',
|
||||
group: 'transactions',
|
||||
name: 'deleteTransaction',
|
||||
description: '/docs/references/vectordb/delete-transaction.md',
|
||||
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
|
||||
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')
|
||||
->inject('queueForDeletes')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\VectorDB\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 'getVectorDBTransaction';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_TRANSACTION;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/vectordb/transactions/:transactionId')
|
||||
->desc('Get transaction')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'documents.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vectorDB',
|
||||
group: 'transactions',
|
||||
name: 'getTransaction',
|
||||
description: '/docs/references/vectordb/get-transaction.md',
|
||||
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
|
||||
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(...));
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\VectorDB\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 'createVectorDBOperations';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_TRANSACTION;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/vectordb/transactions/:transactionId/operations')
|
||||
->desc('Create operations')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'documents.write')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vectorDB',
|
||||
group: 'transactions',
|
||||
name: 'createOperations',
|
||||
description: '/docs/references/vectordb/create-operations.md',
|
||||
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
|
||||
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: 'documentsdb')), 'Array of staged operations.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\VectorDB\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 'updateVectorDBTransaction';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_TRANSACTION;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/vectordb/transactions/:transactionId')
|
||||
->desc('Update transaction')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'documents.write')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vectorDB',
|
||||
group: 'transactions',
|
||||
name: 'updateTransaction',
|
||||
description: '/docs/references/vectordb/update-transaction.md',
|
||||
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
|
||||
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('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('getDatabasesDB')
|
||||
->inject('user')
|
||||
->inject('transactionState')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForWebhooks')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\VectorDB\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 'listVectorDBTransactions';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_TRANSACTION_LIST;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/vectordb/transactions')
|
||||
->desc('List transactions')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'documents.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vectorDB',
|
||||
group: 'transactions',
|
||||
name: 'listTransactions',
|
||||
description: '/docs/references/vectordb/list-transactions.md',
|
||||
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
|
||||
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(...));
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,12 @@ use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\XList as ListC
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Create as CreateVectorDatabase;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Delete as DeleteVectorDatabase;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Get as GetVectorDatabase;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Transactions\Create as CreateTransaction;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Transactions\Delete as DeleteTransaction;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Transactions\Get as GetTransaction;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Transactions\Operations\Create as CreateOperations;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Transactions\Update as UpdateTransaction;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Transactions\XList as ListTransactions;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Update as UpdateVectorDatabase;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Usage\Get as GetVectorDatabaseUsage;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Usage\XList as ListVectorDatabaseUsage;
|
||||
@@ -41,7 +47,7 @@ class VectorDB extends Base
|
||||
$this->registerIndexActions($service);
|
||||
$this->registerDocumentActions($service);
|
||||
$this->registerEmbeddingActions($service);
|
||||
// $this->registerTransactionActions($service);
|
||||
$this->registerTransactionActions($service);
|
||||
}
|
||||
|
||||
private function registerDatabaseActions(Service $service): void
|
||||
@@ -87,6 +93,16 @@ class VectorDB extends Base
|
||||
$service->addAction(DeleteDocuments::getName(), new DeleteDocuments());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
private function registerEmbeddingActions(Service $service): void
|
||||
{
|
||||
$service->addAction(CreateTextEmbeddings::getName(), new CreateTextEmbeddings());
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Databases\VectorDB\Transactions;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideClient;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
|
||||
class ACIDTest extends Scope
|
||||
{
|
||||
use ProjectCustom;
|
||||
use SideClient;
|
||||
|
||||
private function generateEmbeddings(int $dimensions = 3, float $value = 0.1): array
|
||||
{
|
||||
$vector = array_fill(0, $dimensions, $value);
|
||||
$vector[0] = 1.0;
|
||||
return $vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test atomicity - all operations succeed or all fail
|
||||
*/
|
||||
public function testAtomicity(): void
|
||||
{
|
||||
// Create database
|
||||
$database = $this->client->call(Client::METHOD_POST, '/vectordb', 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 for the test
|
||||
$collection = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'AtomicityTest',
|
||||
'dimensions' => 3,
|
||||
'documentSecurity' => false,
|
||||
'permissions' => [
|
||||
Permission::create(Role::any()),
|
||||
Permission::read(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $collection['headers']['status-code']);
|
||||
$collectionId = $collection['body']['$id'];
|
||||
|
||||
// Create a document outside the transaction
|
||||
$existingDocumentId = 'existing_doc';
|
||||
$doc1 = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'documentId' => $existingDocumentId,
|
||||
'data' => [
|
||||
'embeddings' => $this->generateEmbeddings(3),
|
||||
'metadata' => ['email' => 'existing@example.com'],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $doc1['headers']['status-code']);
|
||||
|
||||
// Create transaction
|
||||
$transaction = $this->client->call(Client::METHOD_POST, '/vectordb/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 create reuses an existing documentId and should cause the commit to fail
|
||||
$response = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'documents' => [
|
||||
[
|
||||
'$id' => 'txn_doc_1',
|
||||
'embeddings' => $this->generateEmbeddings(3, 0.2),
|
||||
'metadata' => ['email' => 'newuser@example.com'],
|
||||
],
|
||||
[
|
||||
'$id' => $existingDocumentId,
|
||||
'embeddings' => $this->generateEmbeddings(3, 0.3),
|
||||
'metadata' => ['email' => 'duplicate@example.com'],
|
||||
],
|
||||
[
|
||||
'$id' => 'txn_doc_2',
|
||||
'embeddings' => $this->generateEmbeddings(3, 0.4),
|
||||
'metadata' => ['email' => 'should-not-exist@example.com'],
|
||||
],
|
||||
],
|
||||
'transactionId' => $transactionId,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code'], 'Adding documents via normal route should succeed. Response: ' . json_encode($response['body']));
|
||||
|
||||
// Attempt to commit - should fail due to duplicate document ID
|
||||
$response = $this->client->call(Client::METHOD_PATCH, "/vectordb/transactions/{$transactionId}", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'commit' => true
|
||||
]);
|
||||
|
||||
$this->assertEquals(409, $response['headers']['status-code']);
|
||||
|
||||
// Verify NO new documents were created (atomicity)
|
||||
$documents = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(1, $documents['body']['total']);
|
||||
$this->assertEquals('existing@example.com', $documents['body']['documents'][0]['metadata']['email']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test consistency - schema validation and constraints
|
||||
*/
|
||||
public function testConsistency(): void
|
||||
{
|
||||
// Create database
|
||||
$database = $this->client->call(Client::METHOD_POST, '/vectordb', 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
|
||||
$collection = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'ConsistencyTest',
|
||||
'dimensions' => 3,
|
||||
'documentSecurity' => false,
|
||||
'permissions' => [
|
||||
Permission::create(Role::any()),
|
||||
Permission::read(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $collection['headers']['status-code']);
|
||||
$collectionId = $collection['body']['$id'];
|
||||
|
||||
// Create transaction
|
||||
$transaction = $this->client->call(Client::METHOD_POST, '/vectordb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]));
|
||||
|
||||
$transactionId = $transaction['body']['$id'];
|
||||
|
||||
// Stage operations with valid and invalid data (embedding length mismatch)
|
||||
$response = $this->client->call(Client::METHOD_POST, "/vectordb/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' => [
|
||||
'embeddings' => $this->generateEmbeddings(3, 0.2),
|
||||
'metadata' => ['name' => 'Valid User'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'databaseId' => $databaseId,
|
||||
'collectionId' => $collectionId,
|
||||
'action' => 'create',
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => $this->generateEmbeddings(2, 0.5), // Invalid dimensions
|
||||
'metadata' => ['name' => 'Invalid User'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'databaseId' => $databaseId,
|
||||
'collectionId' => $collectionId,
|
||||
'action' => 'create',
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => $this->generateEmbeddings(3, 0.6),
|
||||
'metadata' => ['name' => 'Should Not Persist'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
// Attempt to commit - should fail due to invalid embeddings
|
||||
$response = $this->client->call(Client::METHOD_PATCH, "/vectordb/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, 409, 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, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", 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, '/vectordb', 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, '/vectordb/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'IsolationTest',
|
||||
'dimensions' => 3,
|
||||
'documentSecurity' => false,
|
||||
'permissions' => [
|
||||
Permission::create(Role::any()),
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$collectionId = $collection['body']['$id'];
|
||||
|
||||
// Create initial document with status metadata
|
||||
$doc = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'documentId' => 'shared_doc',
|
||||
'data' => [
|
||||
'embeddings' => $this->generateEmbeddings(3),
|
||||
'metadata' => ['status' => 'pending'],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $doc['headers']['status-code']);
|
||||
|
||||
// Create first transaction
|
||||
$transaction1 = $this->client->call(Client::METHOD_POST, '/vectordb/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'];
|
||||
|
||||
// Transaction 1: update status to approved
|
||||
$this->client->call(Client::METHOD_POST, "/vectordb/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,
|
||||
'action' => 'update',
|
||||
'documentId' => 'shared_doc',
|
||||
'data' => [
|
||||
'metadata' => ['status' => 'approved'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Commit first transaction
|
||||
$response1 = $this->client->call(Client::METHOD_PATCH, "/vectordb/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']);
|
||||
|
||||
// Document should reflect the first transaction's update
|
||||
$document = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/shared_doc", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals('approved', $document['body']['metadata']['status']);
|
||||
|
||||
// Create second transaction after first commit
|
||||
$transaction2 = $this->client->call(Client::METHOD_POST, '/vectordb/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 2: update status to declined
|
||||
$this->client->call(Client::METHOD_POST, "/vectordb/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,
|
||||
'action' => 'update',
|
||||
'documentId' => 'shared_doc',
|
||||
'data' => [
|
||||
'metadata' => ['status' => 'declined'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Commit second transaction and ensure isolation guarantees
|
||||
$response2 = $this->client->call(Client::METHOD_PATCH, "/vectordb/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']);
|
||||
|
||||
// Final document should reflect the second transaction's update
|
||||
$document = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/shared_doc", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals('declined', $document['body']['metadata']['status']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test durability - committed data persists
|
||||
*/
|
||||
public function testDurability(): void
|
||||
{
|
||||
// Create database
|
||||
$database = $this->client->call(Client::METHOD_POST, '/vectordb', 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, '/vectordb/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'DurabilityTest',
|
||||
'dimensions' => 3,
|
||||
'documentSecurity' => false,
|
||||
'permissions' => [
|
||||
Permission::create(Role::any()),
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $collection['headers']['status-code']);
|
||||
$collectionId = $collection['body']['$id'];
|
||||
|
||||
// Create transaction with multiple operations
|
||||
$transaction = $this->client->call(Client::METHOD_POST, '/vectordb/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'];
|
||||
|
||||
// Create two documents via normal route inside transaction
|
||||
$this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'documents' => [
|
||||
[
|
||||
'$id' => 'durable_doc_1',
|
||||
'embeddings' => $this->generateEmbeddings(3, 0.3),
|
||||
'metadata' => ['data' => 'Important data 1'],
|
||||
],
|
||||
[
|
||||
'$id' => 'durable_doc_2',
|
||||
'embeddings' => $this->generateEmbeddings(3, 0.5),
|
||||
'metadata' => ['data' => 'Important data 2'],
|
||||
],
|
||||
],
|
||||
'transactionId' => $transactionId,
|
||||
]);
|
||||
|
||||
// Update first document inside the same transaction
|
||||
$this->client->call(Client::METHOD_PATCH, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/durable_doc_1", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'data' => [
|
||||
'metadata' => ['data' => 'Updated important data 1'],
|
||||
],
|
||||
'transactionId' => $transactionId,
|
||||
]);
|
||||
|
||||
// Commit transaction
|
||||
$response = $this->client->call(Client::METHOD_PATCH, "/vectordb/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']);
|
||||
|
||||
// Verify documents exist and have correct data
|
||||
$document1 = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/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']['metadata']['data']);
|
||||
|
||||
$document2 = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/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']['metadata']['data']);
|
||||
|
||||
// Further update outside transaction to ensure persistence
|
||||
$update = $this->client->call(Client::METHOD_PATCH, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/durable_doc_1", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'data' => [
|
||||
'metadata' => ['data' => 'Modified outside transaction'],
|
||||
],
|
||||
]);
|
||||
$this->assertEquals(200, $update['headers']['status-code']);
|
||||
|
||||
// Verify the update persisted
|
||||
$document1 = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/durable_doc_1", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals('Modified outside transaction', $document1['body']['metadata']['data']);
|
||||
|
||||
// List all documents to verify total count
|
||||
$documents = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", 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
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Databases\VectorDB\Transactions;
|
||||
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideConsole;
|
||||
|
||||
class TransactionsConsoleClientTest extends Scope
|
||||
{
|
||||
use TransactionsBase;
|
||||
use ProjectCustom;
|
||||
use SideConsole;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Databases\VectorDB\Transactions;
|
||||
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideClient;
|
||||
|
||||
class TransactionsCustomClientTest extends Scope
|
||||
{
|
||||
use TransactionsBase;
|
||||
use ProjectCustom;
|
||||
use SideClient;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Databases\VectorDB\Transactions;
|
||||
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideServer;
|
||||
|
||||
class TransactionsCustomServerTest extends Scope
|
||||
{
|
||||
use TransactionsBase;
|
||||
use ProjectCustom;
|
||||
use SideServer;
|
||||
}
|
||||
Reference in New Issue
Block a user