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' => 'TransactionTestDatabase' ]); $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; // Test creating a transaction with default TTL $response = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $response['headers']['status-code']); $this->assertArrayHasKey('$id', $response['body']); $this->assertArrayHasKey('status', $response['body']); $this->assertArrayHasKey('operations', $response['body']); $this->assertArrayHasKey('expiresAt', $response['body']); $this->assertEquals('pending', $response['body']['status']); $this->assertEquals(0, $response['body']['operations']); $transactionId1 = $response['body']['$id']; // Test creating a transaction with custom TTL $response = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'ttl' => 900 ]); $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals('pending', $response['body']['status']); $expiresAt = new \DateTime($response['body']['expiresAt']); $now = new \DateTime(); $diff = $expiresAt->getTimestamp() - $now->getTimestamp(); $this->assertGreaterThan(800, $diff); $this->assertLessThan(1000, $diff); $transactionId2 = $response['body']['$id']; // Test invalid TTL values $response = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'ttl' => 30 // Below minimum ]); $this->assertEquals(400, $response['headers']['status-code']); $response = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'ttl' => 4000 // Above maximum ]); $this->assertEquals(400, $response['headers']['status-code']); } /** * Test adding operations to a transaction */ public function testCreateOperations(): void { // Create database first $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' => 'TransactionOperationsTestDB' ]); $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; // Create a table for testing $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' => ID::unique(), 'name' => 'TransactionOperationsTest', 'rowSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $this->assertEquals(201, $table['headers']['status-code']); $tableId = $table['body']['$id']; // Add columns $column = $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' => 'name', 'size' => 256, 'required' => true, ]); $this->assertEquals(202, $column['headers']['status-code']); // Wait for column to be created sleep(2); // Add valid operations $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, 'tableId' => $tableId, 'action' => 'create', 'rowId' => 'doc1', 'data' => [ 'name' => 'Test Document 1' ] ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'create', 'rowId' => 'doc2', 'data' => [ 'name' => 'Test Document 2' ] ] ] ]); $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals(2, $response['body']['operations']); // Test adding more operations $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, 'tableId' => $tableId, 'action' => 'update', 'rowId' => 'doc1', 'data' => [ 'name' => 'Updated Document 1' ] ] ] ]); $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals(3, $response['body']['operations']); // Test invalid database ID $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' => 'invalid_database', 'tableId' => $tableId, 'action' => 'create', 'rowId' => ID::unique(), 'data' => ['name' => 'Test'] ] ] ]); $this->assertEquals(404, $response['headers']['status-code'], 'Invalid database should return 404. Got: ' . json_encode($response['body'])); // Test invalid table ID $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, 'tableId' => 'invalid_table', 'action' => 'create', 'rowId' => ID::unique(), 'data' => ['name' => 'Test'] ] ] ]); $this->assertEquals(404, $response['headers']['status-code']); } /** * Test committing a transaction */ public function testCommit(): void { // Create database first $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' => 'TransactionCommitTestDB' ]); $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; // Create table $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' => ID::unique(), 'name' => 'TransactionCommitTest', 'rowSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $this->assertEquals(201, $table['headers']['status-code']); $tableId = $table['body']['$id']; // Add columns $column = $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' => 'name', 'size' => 256, 'required' => true, ]); $this->assertEquals(202, $column['headers']['status-code']); sleep(2); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; // Add operations $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, 'tableId' => $tableId, 'action' => 'create', 'rowId' => 'doc1', 'data' => [ 'name' => 'Test Document 1' ] ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'create', 'rowId' => 'doc2', 'data' => [ 'name' => 'Test Document 2' ] ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'update', 'rowId' => 'doc1', 'data' => [ 'name' => 'Updated Document 1' ] ] ] ]); $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals(3, $response['body']['operations']); // Commit the 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']); $this->assertEquals('committed', $response['body']['status']); // Verify rows were created $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $rows['headers']['status-code']); $this->assertEquals(2, $rows['body']['total']); // Verify the update was applied $doc1Found = false; foreach ($rows['body']['rows'] as $doc) { if ($doc['$id'] === 'doc1') { $this->assertEquals('Updated Document 1', $doc['name']); $doc1Found = true; } } $this->assertTrue($doc1Found, 'Document doc1 should exist with updated name'); // Test committing already committed 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(400, $response['headers']['status-code']); } /** * Test rolling back a transaction */ public function testRollback(): void { // Create database first $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' => 'TransactionRollbackTestDB' ]); $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; // Create a table for rollback test $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' => ID::unique(), 'name' => 'TransactionRollbackTest', 'rowSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Add column $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' => 'value', 'size' => 256, 'required' => true, ]); sleep(2); // Add operations $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, 'tableId' => $tableId, 'action' => 'create', 'rowId' => 'rollback_doc', 'data' => [ 'value' => 'Should not exist' ] ] ] ]); $this->assertEquals(201, $response['headers']['status-code']); // Rollback the 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'] ]), [ 'rollback' => true ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('failed', $response['body']['status']); // Verify no rows were created $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $rows['headers']['status-code']); $this->assertEquals(0, $rows['body']['total']); } /** * Test transaction expiration */ public function testTransactionExpiration(): void { // Create database and table $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' => 'ExpirationTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create column $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' => 'data', 'size' => 256, 'required' => false, ]); sleep(2); // Create transaction with minimum TTL (60 seconds) $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'ttl' => 60 ]); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; // Add operation $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, 'tableId' => $tableId, 'action' => 'create', 'rowId' => ID::unique(), 'data' => ['data' => 'Should expire'] ] ] ]); $this->assertEquals(201, $response['headers']['status-code']); // Verify transaction was created with correct expiration $txnDetails = $this->client->call(Client::METHOD_GET, "/tablesdb/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ])); $this->assertEquals(200, $txnDetails['headers']['status-code']); $this->assertEquals('pending', $txnDetails['body']['status']); // Verify expiration time is approximately 60 seconds from now $expiresAt = new \DateTime($txnDetails['body']['expiresAt']); $now = new \DateTime(); $diff = $expiresAt->getTimestamp() - $now->getTimestamp(); $this->assertGreaterThan(55, $diff); $this->assertLessThan(65, $diff); } /** * Test maximum operations per transaction */ public function testTransactionSizeLimit(): void { // Create database and table $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' => 'SizeLimitTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [Permission::create(Role::any())], ]); $tableId = $table['body']['$id']; // Create column $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' => 'value', 'size' => 256, 'required' => false, ]); sleep(2); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Try to add operations exceeding the limit (assuming limit is 100) // We'll add 50 operations twice to test incremental limit $operations = []; for ($i = 0; $i < 50; $i++) { $operations[] = [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'create', 'rowId' => 'doc_' . $i, 'data' => ['value' => 'Test ' . $i] ]; } // First batch should succeed $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' => $operations ]); $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals(50, $response['body']['operations']); // Second batch of 50 more operations $operations = []; for ($i = 50; $i < 100; $i++) { $operations[] = [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'rowId' => 'doc_' . $i, 'action' => 'create', 'data' => ['value' => 'Test ' . $i] ]; } $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' => $operations ]); $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals(100, $response['body']['operations']); // Try to add one more operation - should fail $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, 'tableId' => $tableId, 'action' => 'create', 'rowId' => 'doc_overflow', 'data' => ['value' => 'This should fail'] ] ] ]); $this->assertEquals(400, $response['headers']['status-code']); } /** * Test concurrent transactions with conflicting operations */ public function testConcurrentTransactionConflicts(): void { // Create database and table $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' => 'ConflictTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create column $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/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 row $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'shared_doc', 'data' => ['counter' => 100] ]); $this->assertEquals(201, $doc['headers']['status-code']); // Create two transactions $txn1 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $txn2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId1 = $txn1['body']['$id']; $transactionId2 = $txn2['body']['$id']; // Both transactions try to update the same row $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, 'tableId' => $tableId, 'action' => 'update', 'rowId' => 'shared_doc', 'data' => ['counter' => 200] ] ] ]); $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, 'tableId' => $tableId, 'action' => 'update', 'rowId' => 'shared_doc', 'data' => ['counter' => 300] ] ] ]); // 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 - should fail with conflict $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(409, $response2['headers']['status-code']); // Conflict // Verify the row has the value from first transaction $doc = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/shared_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $doc['body']['counter']); } /** * Test deleting a row that's being updated in a transaction */ public function testDeleteDocumentDuringTransaction(): void { // Create database and table $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' => 'DeleteConflictDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create column $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' => 'data', 'size' => 256, 'required' => false, ]); sleep(2); // Create row $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'target_doc', 'data' => ['data' => 'Original'] ]); $this->assertEquals(201, $doc['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'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Add update operation to transaction $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, 'tableId' => $tableId, 'action' => 'update', 'rowId' => 'target_doc', 'data' => ['data' => 'Updated in transaction'] ] ] ]); // Delete the row outside of transaction $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/target_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ])); $this->assertEquals(204, $response['headers']['status-code']); // Try to commit transaction - should fail because row no longer exists $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(404, $response['headers']['status-code']); } /** * Test bulk operations in transactions */ public function testBulkOperations(): void { // Create database and table $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' => 'BulkOpsDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns $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' => 'name', 'size' => 256, 'required' => true, ]); $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' => 'category', 'size' => 256, 'required' => true, ]); sleep(3); // Create some initial rows for ($i = 1; $i <= 5; $i++) { $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'existing_' . $i, 'data' => [ 'name' => 'Existing ' . $i, 'category' => 'old' ] ]); } // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Add bulk operations $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' => [ // Bulk create [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'bulkCreate', 'data' => [ ['$id' => 'bulk_1', 'name' => 'Bulk 1', 'category' => 'new'], ['$id' => 'bulk_2', 'name' => 'Bulk 2', 'category' => 'new'], ['$id' => 'bulk_3', 'name' => 'Bulk 3', 'category' => 'new'], ] ], // Bulk update [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'bulkUpdate', 'data' => [ 'queries' => [Query::equal('category', ['old'])->toString()], 'data' => ['category' => 'updated'] ] ], // Bulk delete [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'bulkDelete', 'data' => [ 'queries' => [Query::equal('name', ['Existing 5'])->toString()] ] ] ] ]); $this->assertEquals(201, $response['headers']['status-code']); // 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']); // Verify results $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); // Should have 7 rows (5 existing - 1 deleted + 3 new) $this->assertEquals(7, $rows['body']['total']); // Check categories were updated $oldCategoryCount = 0; $updatedCategoryCount = 0; $newCategoryCount = 0; foreach ($rows['body']['rows'] as $doc) { switch ($doc['category']) { case 'old': $oldCategoryCount++; break; case 'updated': $updatedCategoryCount++; break; case 'new': $newCategoryCount++; break; } } $this->assertEquals(0, $oldCategoryCount); $this->assertEquals(4, $updatedCategoryCount); // 4 existing docs updated $this->assertEquals(3, $newCategoryCount); // 3 new docs } /** * Test transaction with mixed success and failure operations */ public function testPartialFailureRollback(): void { // Create database and table $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' => 'PartialFailureDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns with constraints $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' => 'email', 'size' => 256, 'required' => true, ]); sleep(2); // Create unique index on email $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/indexes", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'unique_email', 'type' => 'unique', 'columns' => ['email'], ]); sleep(2); // Create an existing row $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => ID::unique(), 'data' => ['email' => 'existing@example.com'] ]); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Add operations - mix of valid and invalid $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, 'tableId' => $tableId, 'action' => 'create', 'rowId' => ID::unique(), 'data' => ['email' => 'valid1@example.com'] // Valid ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'create', 'rowId' => ID::unique(), 'data' => ['email' => 'valid2@example.com'] // Valid ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'create', 'rowId' => ID::unique(), 'data' => ['email' => 'existing@example.com'] // Will fail - duplicate ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'create', 'rowId' => ID::unique(), 'data' => ['email' => 'valid3@example.com'] // Would be valid but should rollback ], ] ]); $this->assertEquals(201, $response['headers']['status-code']); // Try to commit - should fail and rollback all operations $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(409, $response['headers']['status-code']); // Conflict due to duplicate // Verify NO new rows were created (atomicity) $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(1, $rows['body']['total']); // Only the original row $this->assertEquals('existing@example.com', $rows['body']['rows'][0]['email']); } /** * Test double commit/rollback attempts */ public function testDoubleCommitRollback(): void { // Create database and table $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' => 'DoubleCommitDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [Permission::create(Role::any())], ]); $tableId = $table['body']['$id']; // Create column $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' => 'data', 'size' => 256, 'required' => false, ]); sleep(2); // Test double commit $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Add operation $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, 'tableId' => $tableId, 'action' => 'create', 'rowId' => ID::unique(), 'data' => ['data' => 'Test'] ] ] ]); // First commit $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']); // Second commit attempt - should fail $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(400, $response['headers']['status-code']); // Bad request - already committed // Test double rollback $transaction2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId2 = $transaction2['body']['$id']; // First rollback $response = $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'] ]), [ 'rollback' => true ]); $this->assertEquals(200, $response['headers']['status-code']); // Second rollback attempt - should fail $response = $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'] ]), [ 'rollback' => true ]); $this->assertEquals(400, $response['headers']['status-code']); // Bad request - already rolled back } /** * Test operations on non-existent rows */ public function testOperationsOnNonExistentDocuments(): void { // Create database and table $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' => 'NonExistentDocDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create column $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' => 'data', 'size' => 256, 'required' => false, ]); sleep(2); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Try to update non-existent row - should fail at staging time with early validation $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, 'tableId' => $tableId, 'action' => 'update', 'rowId' => 'non_existent_doc', 'data' => ['data' => 'Should fail'] ] ] ]); $this->assertEquals(404, $response['headers']['status-code']); // Document not found at staging time // Test delete non-existent row - should also fail at staging time with early validation $transaction2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId2 = $transaction2['body']['$id']; $response = $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, 'tableId' => $tableId, 'action' => 'delete', 'rowId' => 'non_existent_doc', 'data' => [] ] ] ]); $this->assertEquals(404, $response['headers']['status-code']); // Document not found at staging time } /** * Test createDocument with transactionId via normal route */ public function testCreateDocument(): void { // Create database and table $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' => 'WriteRoutesTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'rowSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns $columns = [ ['key' => 'name', 'type' => 'string', 'size' => 256, 'required' => true], ['key' => 'counter', 'type' => 'integer', 'required' => false, 'min' => 0, 'max' => 10000], ['key' => 'category', 'type' => 'string', 'size' => 256, 'required' => false], ['key' => 'data', 'type' => 'string', 'size' => 256, 'required' => false], ]; foreach ($columns as $attr) { $type = $attr['type']; unset($attr['type']); $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/{$type}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), $attr); $this->assertEquals(202, $response['headers']['status-code']); } 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'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; // Create row via normal route with transactionId $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'doc_from_route', 'data' => [ 'name' => 'Created via normal route', 'counter' => 100, 'category' => 'test' ], 'transactionId' => $transactionId ]); $this->assertEquals(201, $response['headers']['status-code']); // Document should not exist outside transaction yet $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_from_route", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code']); // 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']); // Document should now exist $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_from_route", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('Created via normal route', $response['body']['name']); } /** * Test updateDocument with transactionId via normal route */ public function testUpdateDocument(): void { // Create database and table $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' => 'UpdateRouteTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns $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' => 'name', 'size' => 256, 'required' => true, ]); $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'counter', 'required' => false, 'min' => 0, 'max' => 10000, ]); $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' => 'category', 'size' => 256, 'required' => false, ]); sleep(3); // Create row outside transaction $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'doc_to_update', 'data' => [ 'name' => 'Original name', 'counter' => 50, 'category' => 'original' ] ]); $this->assertEquals(201, $doc['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'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Update row via normal route with transactionId $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_to_update", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'data' => [ 'name' => 'Updated via normal route', 'counter' => 150, 'category' => 'updated' ], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); // Document should still have original values outside transaction $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_to_update", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals('Original name', $response['body']['name']); $this->assertEquals(50, $response['body']['counter']); // 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']); // Document should now have updated values $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_to_update", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals('Updated via normal route', $response['body']['name']); $this->assertEquals(150, $response['body']['counter']); } /** * Test upsertDocument with transactionId via normal route */ public function testUpsertDocument(): void { // Create database and table $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' => 'UpsertRouteTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns $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' => 'name', 'size' => 256, 'required' => true, ]); $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'counter', 'required' => false, 'min' => 0, 'max' => 10000, ]); 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'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Upsert row (create) via normal route with transactionId $response = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_upsert", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'doc_upsert', 'data' => [ 'name' => 'Created by upsert', 'counter' => 25 ], 'transactionId' => $transactionId ]); $this->assertEquals(201, $response['headers']['status-code']); // Document should not exist outside transaction yet $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_upsert", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code']); // Upsert same row (update) in same transaction $response = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_upsert", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'doc_upsert', 'data' => [ 'name' => 'Updated by upsert', 'counter' => 75 ], 'transactionId' => $transactionId ]); $this->assertEquals(201, $response['headers']['status-code']); // Upsert in transaction returns 201 // 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']); // Document should now exist with updated values $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_upsert", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('Updated by upsert', $response['body']['name']); $this->assertEquals(75, $response['body']['counter']); } /** * Test deleteDocument with transactionId via normal route */ public function testDeleteDocument(): void { // Create database and table $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' => 'DeleteRouteTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create column $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' => 'name', 'size' => 256, 'required' => true, ]); sleep(2); // Create row outside transaction $doc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'doc_to_delete', 'data' => ['name' => 'Will be deleted'] ]); $this->assertEquals(201, $doc['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'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Delete row via normal route with transactionId $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_to_delete", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'transactionId' => $transactionId ]); $this->assertEquals(204, $response['headers']['status-code']); // Document should still exist outside transaction $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_to_delete", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); // 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']); // Document should no longer exist $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_to_delete", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code']); } /** * Test bulkCreate with transactionId via normal route */ public function testBulkCreate(): void { // Create database and table $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' => 'BulkCreateTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns $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' => 'name', 'size' => 256, 'required' => true, ]); $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' => 'category', 'size' => 256, 'required' => false, ]); 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'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Bulk create via normal route with transactionId $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rows' => [ [ '$id' => 'bulk_create_1', 'name' => 'Bulk created 1', 'category' => 'bulk_created' ], [ '$id' => 'bulk_create_2', 'name' => 'Bulk created 2', 'category' => 'bulk_created' ], [ '$id' => 'bulk_create_3', 'name' => 'Bulk created 3', 'category' => 'bulk_created' ] ], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); // Bulk operations return 200 // Documents should not exist outside transaction yet $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [Query::equal('category', ['bulk_created'])->toString()] ]); $this->assertEquals(0, $response['body']['total']); // Individual row check $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/bulk_create_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code']); // 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']); // Documents should now exist $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [Query::equal('category', ['bulk_created'])->toString()] ]); $this->assertEquals(3, $response['body']['total']); // Verify individual rows for ($i = 1; $i <= 3; $i++) { $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/bulk_create_{$i}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals("Bulk created {$i}", $response['body']['name']); $this->assertEquals('bulk_created', $response['body']['category']); } } /** * Test bulkUpdate with transactionId via normal route */ public function testBulkUpdate(): void { // Create database and table $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' => 'BulkUpdateTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns $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' => 'name', 'size' => 256, 'required' => true, ]); $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' => 'category', 'size' => 256, 'required' => false, ]); sleep(3); // Create rows for bulk testing for ($i = 1; $i <= 3; $i++) { $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'bulk_update_' . $i, 'data' => [ 'name' => 'Bulk doc ' . $i, 'category' => 'bulk_test' ] ]); } // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Bulk update via normal route with transactionId $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'queries' => [Query::equal('category', ['bulk_test'])->toString()], 'data' => ['category' => 'bulk_updated'], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); // Documents should still have original category outside transaction $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [Query::equal('category', ['bulk_test'])->toString()] ]); $this->assertEquals(3, $response['body']['total']); // 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']); // Documents should now have updated category $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [Query::equal('category', ['bulk_updated'])->toString()] ]); $this->assertEquals(3, $response['body']['total']); } /** * Test bulkUpsert with transactionId via normal route */ public function testBulkUpsert(): void { // Create database and table $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' => 'BulkUpsertTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns $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' => 'name', 'size' => 256, 'required' => true, ]); $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'counter', 'required' => false, 'min' => 0, 'max' => 10000, ]); sleep(3); // Create one row outside transaction $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'bulk_upsert_existing', 'data' => [ 'name' => 'Existing doc', 'counter' => 10 ] ]); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Bulk upsert via normal route with transactionId (updates existing, creates new) $response = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rows' => [ [ '$id' => 'bulk_upsert_existing', 'name' => 'Updated existing', 'counter' => 20 ], [ '$id' => 'bulk_upsert_new', 'name' => 'New doc', 'counter' => 30 ] ], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); // Original row should be unchanged, new row shouldn't exist outside transaction $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/bulk_upsert_existing", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals('Existing doc', $response['body']['name']); $this->assertEquals(10, $response['body']['counter']); $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/bulk_upsert_new", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code']); // 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']); // Check both rows exist with updated values $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/bulk_upsert_existing", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals('Updated existing', $response['body']['name']); $this->assertEquals(20, $response['body']['counter']); $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/bulk_upsert_new", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals('New doc', $response['body']['name']); $this->assertEquals(30, $response['body']['counter']); } /** * Test bulkDelete with transactionId via normal route */ public function testBulkDelete(): void { // Create database and table $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' => 'BulkDeleteTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns $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' => 'name', 'size' => 256, 'required' => true, ]); $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' => 'category', 'size' => 256, 'required' => false, ]); sleep(3); // Create rows for bulk testing for ($i = 1; $i <= 3; $i++) { $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'bulk_delete_' . $i, 'data' => [ 'name' => 'Delete doc ' . $i, 'category' => 'bulk_delete_test' ] ]); } // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Bulk delete via normal route with transactionId $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'queries' => [Query::equal('category', ['bulk_delete_test'])->toString()], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); // Bulk delete with transaction returns 200 // Documents should still exist outside transaction $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [Query::equal('category', ['bulk_delete_test'])->toString()] ]); $this->assertEquals(3, $response['body']['total']); // 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']); // Documents should now be deleted $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [Query::equal('category', ['bulk_delete_test'])->toString()] ]); $this->assertEquals(0, $response['body']['total']); } /** * Test multiple single route operations in one transaction */ public function testMixedSingleOperations(): void { // Create database and table $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' => 'MultipleSingleRoutesDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns $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' => 'name', 'size' => 256, 'required' => true, ]); $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' => 'status', 'size' => 256, 'required' => false, ]); $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'priority', 'required' => false, 'min' => 1, 'max' => 10, ]); sleep(3); // Create an existing row outside transaction for testing $existingDoc = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'existing_doc', 'data' => [ 'name' => 'Existing Document', 'status' => 'active', 'priority' => 5 ] ]); $this->assertEquals(201, $existingDoc['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'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; $this->assertEquals(201, $transaction['headers']['status-code']); // 1. Create new row via normal route with transactionId $response1 = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'new_doc_1', 'data' => [ 'name' => 'New Document 1', 'status' => 'pending', 'priority' => 1 ], 'transactionId' => $transactionId ]); $this->assertEquals(201, $response1['headers']['status-code']); // 2. Create another row via normal route with transactionId $response2 = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'new_doc_2', 'data' => [ 'name' => 'New Document 2', 'status' => 'pending', 'priority' => 2 ], 'transactionId' => $transactionId ]); $this->assertEquals(201, $response2['headers']['status-code']); // 3. Update existing row via normal route with transactionId $response3 = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/existing_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'data' => [ 'status' => 'updated', 'priority' => 10 ], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response3['headers']['status-code']); // 4. Update the first new row (created in same transaction) $response4 = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/new_doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'data' => [ 'status' => 'active', 'priority' => 8 ], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response4['headers']['status-code']); // 5. Delete the second new row (created in same transaction) $response5 = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/new_doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'transactionId' => $transactionId ]); $this->assertEquals(204, $response5['headers']['status-code']); // 6. Upsert a new row via normal route with transactionId $response6 = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/upserted_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'upserted_doc', 'data' => [ 'name' => 'Upserted Document', 'status' => 'new', 'priority' => 3 ], 'transactionId' => $transactionId ]); $this->assertEquals(201, $response6['headers']['status-code']); // Check transaction has correct number of operations $txnDetails = $this->client->call(Client::METHOD_GET, "/tablesdb/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ])); $this->assertEquals(200, $txnDetails['headers']['status-code']); $this->assertEquals(6, $txnDetails['body']['operations']); // 6 operations total // Verify nothing exists outside transaction yet $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/new_doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code']); $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/upserted_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code']); // Existing doc should still have original values $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/existing_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals('active', $response['body']['status']); $this->assertEquals(5, $response['body']['priority']); // 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']); $this->assertEquals('committed', $response['body']['status']); // Verify final state after commit // new_doc_1 should exist with updated values $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/new_doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('New Document 1', $response['body']['name']); $this->assertEquals('active', $response['body']['status']); $this->assertEquals(8, $response['body']['priority']); // new_doc_2 should not exist (was deleted in transaction) $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/new_doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code']); // existing_doc should have updated values $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/existing_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals('updated', $response['body']['status']); $this->assertEquals(10, $response['body']['priority']); // upserted_doc should exist $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/upserted_doc", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('Upserted Document', $response['body']['name']); $this->assertEquals('new', $response['body']['status']); $this->assertEquals(3, $response['body']['priority']); // Verify total row count $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(3, $rows['body']['total']); // existing_doc, new_doc_1, upserted_doc } /** * Test mixed operations with transactions */ public function testMixedOperations(): void { // Create database and table $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' => 'MixedOpsTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create column $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' => 'name', 'size' => 256, 'required' => true, ]); sleep(2); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Add operation via Operations\Add endpoint $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, 'tableId' => $tableId, 'action' => 'create', 'rowId' => 'mixed_doc1', 'data' => ['name' => 'Via Operations Add'] ] ] ]); $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals(1, $response['body']['operations']); // Add operation via normal route with transactionId $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'mixed_doc2', 'data' => ['name' => 'Via normal route'], 'transactionId' => $transactionId ]); $this->assertEquals(201, $response['headers']['status-code']); // Check transaction now has 2 operations $txnDetails = $this->client->call(Client::METHOD_GET, "/tablesdb/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ])); $this->assertEquals(2, $txnDetails['body']['operations']); // Both rows shouldn't exist yet $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/mixed_doc1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code']); $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/mixed_doc2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code']); // 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']); // Both rows should now exist $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/mixed_doc1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('Via Operations Add', $response['body']['name']); $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/mixed_doc2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('Via normal route', $response['body']['name']); } /** * Test bulk update with queries that should match rows created in the same transaction */ public function testBulkUpdateWithTransactionAwareQueries(): void { // Create database and table $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' => 'BulkTxnAwareDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns $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' => 'name', 'size' => 256, 'required' => true, ]); $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'age', 'required' => true, ]); $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' => 'status', 'size' => 256, 'required' => true, ]); sleep(3); // Wait for columns to be created // Create some existing rows for ($i = 1; $i <= 3; $i++) { $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'existing_' . $i, 'data' => [ 'name' => 'Existing ' . $i, 'age' => 20 + $i, 'status' => 'inactive' ] ]); } // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Step 1: Create new rows with age > 25 in transaction $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'txn_doc_1', 'data' => [ 'name' => 'Transaction Doc 1', 'age' => 30, 'status' => 'inactive' ], 'transactionId' => $transactionId ]); $this->assertEquals(201, $response['headers']['status-code']); $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'txn_doc_2', 'data' => [ 'name' => 'Transaction Doc 2', 'age' => 35, 'status' => 'inactive' ], 'transactionId' => $transactionId ]); $this->assertEquals(201, $response['headers']['status-code']); // Step 2: Bulk update all rows with age > 25 to have status 'active' // This should match both existing_3 (age=23 doesn't match, age=24 doesn't match, but existing rows have age 21,22,23) // Wait, let me fix the ages - existing docs have ages 21, 22, 23, so only txn docs should match $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'data' => [ 'status' => 'active' ], 'queries' => [Query::greaterThan('age', 25)->toString()], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); // 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']); // Verify that rows created in the transaction were updated by the bulk update $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/txn_doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('active', $response['body']['status'], 'Document created in transaction should be updated by bulk update query'); $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/txn_doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('active', $response['body']['status'], 'Document created in transaction should be updated by bulk update query'); // Verify existing rows were not affected for ($i = 1; $i <= 3; $i++) { $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/existing_{$i}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('inactive', $response['body']['status'], "Existing row {$i} should remain inactive (age <= 25)"); } } /** * Test bulk update with queries that should match rows updated in the same transaction */ public function testBulkUpdateMatchingUpdatedDocuments(): void { // Create database and table $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' => 'BulkUpdateTxnDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns $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' => 'name', 'size' => 256, 'required' => true, ]); $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' => 'category', 'size' => 256, 'required' => true, ]); $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' => 'priority', 'size' => 256, 'required' => true, ]); sleep(3); // Wait for columns to be created // Create existing rows for ($i = 1; $i <= 4; $i++) { $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'doc_' . $i, 'data' => [ 'name' => 'Document ' . $i, 'category' => 'normal', 'priority' => 'low' ] ]); } // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Step 1: Update some rows to have category 'special' in transaction $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'data' => [ 'category' => 'special' ], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'data' => [ 'category' => 'special' ], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); // Step 2: Bulk update all rows with category 'special' to have priority 'high' // This should match the rows we just updated in the transaction $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'data' => [ 'priority' => 'high' ], 'queries' => [Query::equal('category', ['special'])->toString()], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); // 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']); // Verify that the updated rows were matched by bulk update $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('special', $response['body']['category']); $this->assertEquals('high', $response['body']['priority'], 'Document updated in transaction should be matched by bulk update query'); $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('special', $response['body']['category']); $this->assertEquals('high', $response['body']['priority'], 'Document updated in transaction should be matched by bulk update query'); // Verify other rows were not affected $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_3", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('normal', $response['body']['category']); $this->assertEquals('low', $response['body']['priority']); } /** * Test bulk delete with queries that should match rows created in the same transaction */ public function testBulkDeleteMatchingCreatedDocuments(): void { // Create database and table $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' => 'BulkDeleteTxnDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns $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' => 'name', 'size' => 256, 'required' => true, ]); $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' => 'type', 'size' => 256, 'required' => true, ]); sleep(3); // Wait for columns to be created // Create existing rows for ($i = 1; $i <= 3; $i++) { $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'existing_' . $i, 'data' => [ 'name' => 'Existing ' . $i, 'type' => 'permanent' ] ]); } // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Step 1: Create temporary rows in transaction $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'temp_1', 'data' => [ 'name' => 'Temporary 1', 'type' => 'temporary' ], 'transactionId' => $transactionId ]); $this->assertEquals(201, $response['headers']['status-code']); $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'temp_2', 'data' => [ 'name' => 'Temporary 2', 'type' => 'temporary' ], 'transactionId' => $transactionId ]); $this->assertEquals(201, $response['headers']['status-code']); // Step 2: Bulk delete all rows with type 'temporary' // This should delete the rows we just created in the transaction $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'queries' => [Query::equal('type', ['temporary'])->toString()], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); // 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']); // Verify temporary rows were deleted (should not exist) $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/temp_1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code'], 'Temporary row created and deleted in transaction should not exist'); $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/temp_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code'], 'Temporary row created and deleted in transaction should not exist'); // Verify existing rows were not affected for ($i = 1; $i <= 3; $i++) { $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/existing_{$i}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code'], "Permanent row {$i} should still exist"); $this->assertEquals('permanent', $response['body']['type']); } } /** * Test bulk delete with queries that should match rows updated in the same transaction */ public function testBulkDeleteMatchingUpdatedDocuments(): void { // Create database and table $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' => 'BulkDeleteUpdateTxnDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns $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' => 'name', 'size' => 256, 'required' => true, ]); $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' => 'status', 'size' => 256, 'required' => true, ]); sleep(3); // Wait for columns to be created // Create existing rows for ($i = 1; $i <= 5; $i++) { $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'doc_' . $i, 'data' => [ 'name' => 'Document ' . $i, 'status' => 'active' ] ]); } // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Step 1: Mark some rows for deletion by updating their status $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'data' => [ 'status' => 'marked_for_deletion' ], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_4", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'data' => [ 'status' => 'marked_for_deletion' ], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); // Step 2: Bulk delete all rows with status 'marked_for_deletion' // This should delete the rows we just updated in the transaction $response = $this->client->call(Client::METHOD_DELETE, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'queries' => [Query::equal('status', ['marked_for_deletion'])->toString()], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); // 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']); // Verify marked rows were deleted $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_2", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code'], 'Document marked for deletion should have been deleted'); $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_4", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(404, $response['headers']['status-code'], 'Document marked for deletion should have been deleted'); // Verify other rows still exist foreach ([1, 3, 5] as $i) { $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc_{$i}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code'], "Document {$i} should still exist"); $this->assertEquals('active', $response['body']['status']); } } /** * Test increment and decrement operations in transaction */ public function testIncrementDecrementOperations(): void { // Create database and table $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' => 'IncrementDecrementTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'CounterTable', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], ]); $tableId = $table['body']['$id']; // Add integer columns $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'counter', 'required' => false, 'default' => 0, ]); $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'score', 'required' => false, 'default' => 100, ]); sleep(2); // Create initial row $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' => 'counter_row', 'data' => [ 'counter' => 10, 'score' => 50 ] ]); $this->assertEquals(201, $row['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'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Add increment and decrement operations $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, 'tableId' => $tableId, 'action' => 'increment', 'rowId' => 'counter_row', 'data' => [ 'column' => 'counter', 'value' => 5, ] ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'decrement', 'rowId' => 'counter_row', 'data' => [ 'column' => 'score', 'value' => 20, ] ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'increment', 'rowId' => 'counter_row', 'data' => [ 'column' => 'counter', 'value' => 3, 'max' => 20 ] ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'decrement', 'rowId' => 'counter_row', 'data' => [ 'column' => 'score', 'value' => 30, 'min' => 0 ] ] ] ]); $this->assertEquals(201, $response['headers']['status-code']); // 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']); // Verify final values $row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/counter_row", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $row['headers']['status-code']); // counter: 10 + 5 + 3 = 18 (capped at 20 max) $this->assertEquals(18, $row['body']['counter']); // score: 50 - 20 - 100 = -70, but min is 0 $this->assertEquals(0, $row['body']['score']); } /** * Test increment followed by update (read-your-writes) * This test ensures that after an increment operation, subsequent operations * in the same transaction can see the incremented value in the transaction state. */ public function testIncrementThenUpdate(): void { // Create database and table $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' => 'IncrementUpdateTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'CounterTable', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], ]); $tableId = $table['body']['$id']; // Add columns $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'counter', 'required' => false, 'default' => 0, ]); $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' => 'status', 'size' => 50, 'required' => false, ]); sleep(2); // Create initial row $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' => 'test_row', 'data' => [ 'counter' => 10, 'status' => 'initial' ] ]); $this->assertEquals(201, $row['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'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Add operations: increment then update // The update operation needs to see the document in transaction state // to properly merge the changes $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, 'tableId' => $tableId, 'action' => 'increment', 'rowId' => 'test_row', 'data' => [ 'column' => 'counter', 'value' => 5, ] ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'update', 'rowId' => 'test_row', 'data' => [ 'status' => 'updated' ] ], ] ]); $this->assertEquals(201, $response['headers']['status-code']); // 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']); // Verify final values - both increment and update should be applied $row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test_row", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $row['headers']['status-code']); $this->assertEquals(15, $row['body']['counter'], 'Counter should be incremented: 10 + 5 = 15'); $this->assertEquals('updated', $row['body']['status'], 'Status should be updated'); } /** * Test individual increment/decrement endpoints with transactions * This test ensures that: * 1. Transaction logs store the correct attribute key ('column' for TablesDB) * 2. Mock responses return the correct ID keys ('$tableId' not '$collectionId') */ public function testIncrementDecrementEndpointsWithTransaction(): void { // Create database and table $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' => 'IncrDecrEndpointTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'AccountsTable', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], ]); $tableId = $table['body']['$id']; // Add balance column $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'balance', 'required' => false, 'default' => 0, ]); sleep(2); // Create initial rows $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' => 'joe', 'data' => ['balance' => 100] ]); $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' => 'jane', 'data' => ['balance' => 50] ]); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; // Test Bug 1: Decrement using individual endpoint - should store 'column' not 'attribute' in transaction log $decrementResponse = $this->client->call( Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/joe/balance/decrement", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'transactionId' => $transactionId, 'value' => 50, 'min' => 0, ] ); // Test Bug 2: Response should return '$tableId' not '$collectionId' $this->assertEquals(200, $decrementResponse['headers']['status-code']); $this->assertArrayHasKey('$tableId', $decrementResponse['body'], 'Response should contain $tableId for TablesDB API'); $this->assertArrayNotHasKey('$collectionId', $decrementResponse['body'], 'Response should not contain $collectionId for TablesDB API'); $this->assertEquals($tableId, $decrementResponse['body']['$tableId']); $this->assertEquals($databaseId, $decrementResponse['body']['$databaseId']); // Test increment endpoint $incrementResponse = $this->client->call( Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/jane/balance/increment", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'transactionId' => $transactionId, 'value' => 50, ] ); $this->assertEquals(200, $incrementResponse['headers']['status-code']); $this->assertArrayHasKey('$tableId', $incrementResponse['body'], 'Response should contain $tableId for TablesDB API'); $this->assertArrayNotHasKey('$collectionId', $incrementResponse['body'], 'Response should not contain $collectionId for TablesDB API'); $this->assertEquals($tableId, $incrementResponse['body']['$tableId']); $this->assertEquals($databaseId, $incrementResponse['body']['$databaseId']); // Commit transaction - this will fail if transaction log has 'attribute' instead of 'column' $commitResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'commit' => true ]); $this->assertEquals(200, $commitResponse['headers']['status-code'], 'Transaction commit should succeed'); // Verify final values $joe = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/joe", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $jane = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/jane", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $joe['headers']['status-code']); $this->assertEquals(50, $joe['body']['balance'], 'Joe should have 100 - 50 = 50'); $this->assertEquals(200, $jane['headers']['status-code']); $this->assertEquals(100, $jane['body']['balance'], 'Jane should have 50 + 50 = 100'); } /** * Test cross-API compatibility: stage operations via TablesDB, commit via Collections API * This ensures fallback logic works when APIs are mixed */ public function testCrossAPIIncrementDecrement(): void { // Create database and table $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' => 'CrossAPITestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'CrossAPITable', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], ]); $tableId = $table['body']['$id']; // Add balance column $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'balance', 'required' => false, 'default' => 0, ]); sleep(2); // Create initial 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' => 'test', 'data' => ['balance' => 100] ]); // Create transaction using TablesDB API $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Stage operations using TablesDB API (will store 'column' key) $this->client->call( Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test/balance/decrement", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'transactionId' => $transactionId, 'value' => 30, ] ); // Commit using Collections API (expects 'attribute' key but should fallback to 'column') $commitResponse = $this->client->call( Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'commit' => true ] ); $this->assertEquals(200, $commitResponse['headers']['status-code'], 'Cross-API commit should succeed'); // Verify final value $row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $row['headers']['status-code']); $this->assertEquals(70, $row['body']['balance'], 'Balance should be 100 - 30 = 70'); } public function testBulkUpdateWithDependentDocuments(): void { // Create database and table $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' => 'BulkUpdateDependentDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestTable', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], ]); $tableId = $table['body']['$id']; // Add columns $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' => 'status', 'size' => 50, 'required' => false, ]); sleep(2); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Create a document, then bulk update it - this triggers the state structure bug $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, 'tableId' => $tableId, 'action' => 'create', 'rowId' => 'doc1', 'data' => [ 'status' => 'pending' ] ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'bulkUpdate', 'data' => [ 'queries' => [], 'data' => [ 'status' => 'approved' ] ] ], ] ]); $this->assertEquals(201, $response['headers']['status-code']); // 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'], 'Bulk update should succeed on dependent documents'); // Verify the document was updated $row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $row['headers']['status-code']); $this->assertEquals('approved', $row['body']['status']); } /** * Test bulk delete with dependent documents (Bug #2 regression test) */ public function testBulkDeleteWithDependentDocuments(): void { $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' => 'BulkDeleteDependentDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestTable', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; $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' => 'name', 'size' => 50, 'required' => false, ]); sleep(2); $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Create then bulk delete $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, 'tableId' => $tableId, 'action' => 'create', 'rowId' => 'doc1', 'data' => [ 'name' => 'Test' ] ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'bulkDelete', 'data' => [ 'queries' => [], ] ], ] ]); $this->assertEquals(201, $response['headers']['status-code']); $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'], 'Bulk delete should succeed on dependent documents'); // Verify document was deleted $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(0, $rows['body']['total']); } /** * Test bulk upsert with dependent documents (Bug #3 regression test) */ public function testBulkUpsertWithDependentDocuments(): void { $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' => 'BulkUpsertDependentDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestTable', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], ]); $tableId = $table['body']['$id']; $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' => 'status', 'size' => 50, 'required' => false, ]); sleep(2); $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Create then bulk upsert same document $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, 'tableId' => $tableId, 'action' => 'create', 'rowId' => 'doc1', 'data' => [ 'status' => 'pending' ] ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'bulkUpsert', 'data' => [ [ '$id' => 'doc1', 'status' => 'approved' ] ] ], ] ]); $this->assertEquals(201, $response['headers']['status-code']); $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'], 'Bulk upsert should succeed on dependent documents'); $row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $row['headers']['status-code']); $this->assertEquals('approved', $row['body']['status']); } /** * Test bulk update operations in transaction */ public function testBulkUpdateOperations(): void { // Create database and table $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' => 'BulkUpdateTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'BulkUpdateTable', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], ]); $tableId = $table['body']['$id']; // Add columns $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' => 'status', 'size' => 50, 'required' => false, ]); $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' => 'category', 'size' => 50, 'required' => false, ]); sleep(2); // Create initial rows for ($i = 1; $i <= 5; $i++) { $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' => "row_{$i}", 'data' => [ 'status' => 'pending', 'category' => $i % 2 === 0 ? 'even' : 'odd' ] ]); } // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Add bulk update operations $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, 'tableId' => $tableId, 'action' => 'bulkUpdate', 'data' => [ 'queries' => [ Query::equal('category', ['even'])->toString() ], 'data' => [ 'status' => 'approved' ] ] ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'bulkUpdate', 'data' => [ 'queries' => [ Query::equal('category', ['odd'])->toString() ], 'data' => [ 'status' => 'rejected' ] ] ] ] ]); $this->assertEquals(201, $response['headers']['status-code']); // 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']); // Verify updates $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); foreach ($rows['body']['rows'] as $row) { if ($row['category'] === 'even') { $this->assertEquals('approved', $row['status']); } else { $this->assertEquals('rejected', $row['status']); } } } /** * Test bulk upsert operations in transaction */ public function testBulkUpsertOperations(): void { // Create database and table $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' => 'BulkUpsertTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'BulkUpsertTable', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], ]); $tableId = $table['body']['$id']; // Add columns $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' => 'name', 'size' => 100, 'required' => false, ]); $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'value', 'required' => false, ]); sleep(2); // Create some initial rows $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' => 'existing_1', 'data' => [ 'name' => 'Existing Row 1', 'value' => 10 ] ]); $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' => 'existing_2', 'data' => [ 'name' => 'Existing Row 2', 'value' => 20 ] ]); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Add bulk upsert operations $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, 'tableId' => $tableId, 'action' => 'bulkUpsert', 'data' => [ [ '$id' => 'existing_1', 'name' => 'Updated Row 1', 'value' => 100 ], [ '$id' => 'new_1', 'name' => 'New Row 1', 'value' => 30 ], [ '$id' => 'new_2', 'name' => 'New Row 2', 'value' => 40 ] ] ] ] ]); $this->assertEquals(201, $response['headers']['status-code']); // 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']); // Verify results $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(4, $rows['body']['total']); $rowMap = []; foreach ($rows['body']['rows'] as $row) { $rowMap[$row['$id']] = $row; } // Verify updated row $this->assertEquals('Updated Row 1', $rowMap['existing_1']['name']); $this->assertEquals(100, $rowMap['existing_1']['value']); // Verify unchanged row $this->assertEquals('Existing Row 2', $rowMap['existing_2']['name']); $this->assertEquals(20, $rowMap['existing_2']['value']); // Verify new rows $this->assertEquals('New Row 1', $rowMap['new_1']['name']); $this->assertEquals(30, $rowMap['new_1']['value']); $this->assertEquals('New Row 2', $rowMap['new_2']['name']); $this->assertEquals(40, $rowMap['new_2']['value']); } /** * Test bulk delete operations in transaction */ public function testBulkDeleteOperations(): void { // Create database and table $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' => 'BulkDeleteTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'BulkDeleteTable', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; // Add columns $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' => 'type', 'size' => 50, 'required' => false, ]); $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'priority', 'required' => false, ]); sleep(2); // Create initial rows for ($i = 1; $i <= 10; $i++) { $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' => "row_{$i}", 'data' => [ 'type' => $i <= 5 ? 'temp' : 'permanent', 'priority' => $i ] ]); } // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Add bulk delete operations $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, 'tableId' => $tableId, 'action' => 'bulkDelete', 'data' => [ 'queries' => [ Query::equal('type', ['temp'])->toString() ] ] ], [ 'databaseId' => $databaseId, 'tableId' => $tableId, 'action' => 'bulkDelete', 'data' => [ 'queries' => [ Query::greaterThan('priority', 8)->toString() ] ] ] ] ]); $this->assertEquals(201, $response['headers']['status-code']); // 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']); // Verify deletions $rows = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); // Should have deleted rows 1-5 (temp) and rows 9-10 (priority > 8) // Remaining should be rows 6-8 $this->assertEquals(3, $rows['body']['total']); $remainingIds = array_map(fn ($row) => $row['$id'], $rows['body']['rows']); sort($remainingIds); $this->assertEquals(['row_6', 'row_7', 'row_8'], $remainingIds); } /** * Test validation for invalid operation inputs */ public function testCreateOperationsValidation(): void { // Create database and table for testing $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' => 'ValidationTestDatabase' ]); $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'ValidationTest', 'rowSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $this->assertEquals(201, $table['headers']['status-code']); $tableId = $table['body']['$id']; // Add required column $column = $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' => 'name', 'size' => 256, 'required' => true, ]); $this->assertEquals(202, $column['headers']['status-code']); // Wait for column to be ready sleep(2); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; // Test 1: Invalid action type $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' => [ [ 'action' => 'invalidAction', 'databaseId' => $databaseId, 'tableId' => $tableId, 'rowId' => ID::unique(), 'data' => ['name' => 'Test'] ] ] ]); $this->assertEquals(400, $response['headers']['status-code']); // Test 2: Missing required action field $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, 'tableId' => $tableId, 'rowId' => ID::unique(), 'data' => ['name' => 'Test'] ] ] ]); $this->assertEquals(400, $response['headers']['status-code']); // Test 3: Missing required databaseId field $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' => [ [ 'action' => 'create', 'tableId' => $tableId, 'rowId' => ID::unique(), 'data' => ['name' => 'Test'] ] ] ]); $this->assertEquals(400, $response['headers']['status-code']); // Test 4: Missing required tableId field $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' => [ [ 'action' => 'create', 'databaseId' => $databaseId, 'rowId' => ID::unique(), 'data' => ['name' => 'Test'] ] ] ]); $this->assertEquals(400, $response['headers']['status-code']); // Test 5: Missing rowId for create operation $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' => [ [ 'action' => 'create', 'databaseId' => $databaseId, 'tableId' => $tableId, 'data' => ['name' => 'Test'] ] ] ]); $this->assertEquals(400, $response['headers']['status-code']); // Test 6: Missing data for create operation $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' => [ [ 'action' => 'create', 'databaseId' => $databaseId, 'tableId' => $tableId, 'rowId' => ID::unique() ] ] ]); $this->assertEquals(400, $response['headers']['status-code']); // Test 7: BulkCreate with non-array 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' => [ [ 'action' => 'bulkCreate', 'databaseId' => $databaseId, 'tableId' => $tableId, 'data' => 'not an array' ] ] ]); $this->assertEquals(400, $response['headers']['status-code']); // Test 8: BulkUpdate with missing queries $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' => [ [ 'action' => 'bulkUpdate', 'databaseId' => $databaseId, 'tableId' => $tableId, 'data' => [ 'data' => ['name' => 'Updated'] ] ] ] ]); $this->assertEquals(400, $response['headers']['status-code']); // Test 9: BulkUpdate with invalid query format $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' => [ [ 'action' => 'bulkUpdate', 'databaseId' => $databaseId, 'tableId' => $tableId, 'data' => [ 'queries' => 'not an array', 'data' => ['name' => 'Updated'] ] ] ] ]); $this->assertEquals(400, $response['headers']['status-code']); // Test 10: BulkDelete with missing queries $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' => [ [ 'action' => 'bulkDelete', 'databaseId' => $databaseId, 'tableId' => $tableId, 'data' => [] ] ] ]); $this->assertEquals(400, $response['headers']['status-code']); // Test 11: Increment with missing attribute $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' => [ [ 'action' => 'increment', 'databaseId' => $databaseId, 'tableId' => $tableId, 'rowId' => ID::unique(), 'data' => ['value' => 1] ] ] ]); $this->assertEquals(400, $response['headers']['status-code']); // Test 12: Decrement with invalid value type $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' => [ [ 'action' => 'decrement', 'databaseId' => $databaseId, 'tableId' => $tableId, 'rowId' => ID::unique(), 'data' => [ 'attribute' => 'counter', 'value' => 'not a number' ] ] ] ]); $this->assertEquals(400, $response['headers']['status-code']); // Test 13: Empty operations array $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' => [] ]); $this->assertEquals(400, $response['headers']['status-code']); // Test 14: Operations not an array $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' => 'not an array' ]); $this->assertEquals(400, $response['headers']['status-code']); } /** * Test validation for committing/rolling back transactions */ public function testCommitRollbackValidation(): void { // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; // Test 1: Missing both commit and rollback $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'] ]), []); $this->assertEquals(400, $response['headers']['status-code']); // Test 2: Both commit and rollback set to true $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, 'rollback' => true ]); $this->assertEquals(400, $response['headers']['status-code']); // Test 3: Invalid transaction ID $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/invalid_id", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'commit' => true ]); $this->assertEquals(404, $response['headers']['status-code']); // Commit the 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']); // Test 4: Attempt to commit already committed 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(400, $response['headers']['status-code']); } /** * Test validation for non-existent resources */ public function testNonExistentResources(): void { // Create database and transaction $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' => 'ResourceTestDatabase' ]); $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; // Test 1: Non-existent database $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' => [ [ 'action' => 'create', 'databaseId' => 'nonExistentDatabase', 'tableId' => 'someTable', 'rowId' => ID::unique(), 'data' => ['name' => 'Test'] ] ] ]); $this->assertEquals(404, $response['headers']['status-code']); // Test 2: Non-existent table $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' => [ [ 'action' => 'create', 'databaseId' => $databaseId, 'tableId' => 'nonExistentTable', 'rowId' => ID::unique(), 'data' => ['name' => 'Test'] ] ] ]); $this->assertEquals(404, $response['headers']['status-code']); } /** * Test that bulkUpdate can match documents created in the same transaction * This tests the fix for the bug where applyBulkUpdateToState was treating * state entries as Documents instead of arrays with 'document' keys */ public function testBulkUpdateMatchesCreatedDocsInSameTransaction(): void { $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' => 'BulkUpdateStateDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestTable', 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $tableId = $table['body']['$id']; $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' => 'status', 'size' => 256, 'required' => true, ]); $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' => 'flag', 'size' => 256, 'required' => false, ]); sleep(3); $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // Create 3 documents with status='pending' in transaction $docIds = []; for ($i = 1; $i <= 3; $i++) { $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => 'test_' . $i, 'data' => [ 'status' => 'pending' ], 'transactionId' => $transactionId ]); $this->assertEquals(201, $response['headers']['status-code']); $docIds[] = $response['body']['$id']; } // Bulk update all documents with status='pending' to add flag='processed' // This should match all 3 documents created above in the same transaction $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'data' => [ 'flag' => 'processed' ], 'queries' => [Query::equal('status', ['pending'])->toString()], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); // 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']); // Verify all 3 documents have the flag set foreach ($docIds as $docId) { $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/{$docId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('pending', $response['body']['status']); $this->assertEquals('processed', $response['body']['flag'], 'Bulk update should have matched document created in same transaction'); } } /** * Test upsert with auto-generated ID followed by update * This tests that the transaction state properly stores the document under its actual ID, * not under null when the ID is auto-generated */ public function testUpsertAutoIdThenUpdate(): void { // Create database and table $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' => 'UpsertAutoIDTestDB' ]); $databaseId = $database['body']['$id']; $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' => ID::unique(), 'name' => 'TestCollection', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), ], ]); $tableId = $table['body']['$id']; // Create columns $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' => 'name', 'size' => 256, 'required' => true, ]); $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'counter', 'required' => false, 'min' => 0, 'max' => 10000, ]); 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'], ], $this->getHeaders())); $transactionId = $transaction['body']['$id']; // First create a document in the transaction $response = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'rowId' => ID::unique(), 'data' => [ 'name' => 'Initial document', 'counter' => 5 ], 'transactionId' => $transactionId ]); $this->assertEquals(201, $response['headers']['status-code']); $docId = $response['body']['$id']; // Now upsert the same document using ID::unique() in the path // The database will recognize it exists and update it, generating a new auto ID if needed // This tests that handleUpsertOperation properly captures the actual document ID $response = $this->client->call(Client::METHOD_PUT, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/{$docId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'data' => [ 'name' => 'Upserted in transaction', 'counter' => 10 ], 'transactionId' => $transactionId ]); $this->assertEquals(201, $response['headers']['status-code']); // Now try to update the same document again in the same transaction // This verifies that the upsert properly stored the document under its actual ID in state $response = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/{$docId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'data' => [ 'name' => 'Updated after upsert', 'counter' => 20 ], 'transactionId' => $transactionId ]); $this->assertEquals(200, $response['headers']['status-code']); // 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']); // Verify the document has the final updated values $response = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/{$docId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('Updated after upsert', $response['body']['name']); $this->assertEquals(20, $response['body']['counter']); } /** * Test array operators in transactions using updateRow with transactionId * This tests the fix for operators not being parsed when stored in transaction logs */ public function testArrayOperatorsWithUpdateRow(): 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' => 'ArrayOperatorsTestDB' ]); $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; // Create table with array column $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' => ID::unique(), 'name' => 'Items', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $this->assertEquals(201, $table['headers']['status-code']); $tableId = $table['body']['$id']; // Create array column $column = $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' => 'items', 'size' => 255, 'required' => false, 'array' => true, ]); $this->assertEquals(202, $column['headers']['status-code']); sleep(2); // Wait for column to be created // Create initial row with some items $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' => 'test-row', 'data' => [ 'items' => ['item1', 'item2', 'item3', 'item4'] ] ]); $this->assertEquals(201, $row['headers']['status-code']); $this->assertEquals(['item1', 'item2', 'item3', 'item4'], $row['body']['items']); // Create transaction $transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; // Test arrayRemove operator $updateResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test-row", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'transactionId' => $transactionId, 'data' => [ 'items' => Operator::arrayRemove('item2')->toString() ] ]); $this->assertEquals(200, $updateResponse['headers']['status-code']); // Test arrayInsert operator $updateResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test-row", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'transactionId' => $transactionId, 'data' => [ 'items' => Operator::arrayInsert(2, 'newItem')->toString() ] ]); $this->assertEquals(200, $updateResponse['headers']['status-code']); // Commit transaction $commitResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'commit' => true ]); $this->assertEquals(200, $commitResponse['headers']['status-code']); // Verify the operations were applied correctly $row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test-row", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $row['headers']['status-code']); // After removing item2: ['item1', 'item3', 'item4'] // After inserting 'newItem' at index 2: ['item1', 'item3', 'newItem', 'item4'] $this->assertEquals(['item1', 'item3', 'newItem', 'item4'], $row['body']['items']); } /** * Test array operators in transactions using createOperations * This tests the fix for operators not being parsed in bulk operation creation */ public function testArrayOperatorsWithCreateOperations(): 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' => 'ArrayOperatorsBulkTestDB' ]); $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; // Create table with array column $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' => ID::unique(), 'name' => 'Tags', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $this->assertEquals(201, $table['headers']['status-code']); $tableId = $table['body']['$id']; // Create array column $column = $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' => 'tags', 'size' => 255, 'required' => false, 'array' => true, ]); $this->assertEquals(202, $column['headers']['status-code']); sleep(2); // Wait for column to be created // Create initial row $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' => 'doc1', 'data' => [ 'tags' => ['php', 'javascript', 'python', 'ruby'] ] ]); $this->assertEquals(201, $row['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'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; // Create operations using bulk createOperations endpoint with array operators $operations = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [ [ 'action' => 'update', 'databaseId' => $databaseId, 'tableId' => $tableId, 'rowId' => 'doc1', 'data' => [ 'tags' => Operator::arrayRemove('javascript')->toString() ] ], [ 'action' => 'update', 'databaseId' => $databaseId, 'tableId' => $tableId, 'rowId' => 'doc1', 'data' => [ 'tags' => Operator::arrayAppend(['go', 'rust'])->toString() ] ] ] ]); $this->assertEquals(201, $operations['headers']['status-code']); $this->assertEquals(2, $operations['body']['operations']); // Commit transaction $commitResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'commit' => true ]); $this->assertEquals(200, $commitResponse['headers']['status-code']); // Verify the operations were applied correctly $row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc1", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $row['headers']['status-code']); // After removing 'javascript': ['php', 'python', 'ruby'] // After appending ['go', 'rust']: ['php', 'python', 'ruby', 'go', 'rust'] $this->assertEquals(['php', 'python', 'ruby', 'go', 'rust'], $row['body']['tags']); } /** * Test multiple array operators in a single transaction * This tests all common array operators to ensure comprehensive coverage */ public function testMultipleArrayOperators(): 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' => 'MultipleOperatorsTestDB' ]); $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; // Create table $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' => ID::unique(), 'name' => 'Arrays', 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $this->assertEquals(201, $table['headers']['status-code']); $tableId = $table['body']['$id']; // Create multiple array columns $columns = [ ['columnId' => 'list1', 'name' => 'List1'], ['columnId' => 'list2', 'name' => 'List2'], ['columnId' => 'list3', 'name' => 'List3'], ]; foreach ($columns as $col) { $column = $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' => $col['columnId'], 'size' => 255, 'required' => false, 'array' => true, ]); $this->assertEquals(202, $column['headers']['status-code']); } sleep(2); // Wait for columns to be created // Create initial row $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' => 'multi-ops', 'data' => [ 'list1' => ['a', 'b', 'c'], 'list2' => ['x', 'y', 'z'], 'list3' => ['1', '2', '3', '4', '5'] ] ]); $this->assertEquals(201, $row['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'], ], $this->getHeaders())); $this->assertEquals(201, $transaction['headers']['status-code']); $transactionId = $transaction['body']['$id']; // Test arrayPrepend $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/multi-ops", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'transactionId' => $transactionId, 'data' => [ 'list1' => Operator::arrayPrepend(['z'])->toString() ] ]); // Test arrayAppend $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/multi-ops", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'transactionId' => $transactionId, 'data' => [ 'list2' => Operator::arrayAppend(['w'])->toString() ] ]); // Test arrayRemove $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/multi-ops", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'transactionId' => $transactionId, 'data' => [ 'list3' => Operator::arrayRemove('3')->toString() ] ]); // Commit transaction $commitResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'commit' => true ]); $this->assertEquals(200, $commitResponse['headers']['status-code']); // Verify all operations were applied correctly $row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/multi-ops", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $row['headers']['status-code']); $this->assertEquals(['z', 'a', 'b', 'c'], $row['body']['list1'], 'arrayPrepend should add element at the beginning'); $this->assertEquals(['x', 'y', 'z', 'w'], $row['body']['list2'], 'arrayAppend should add element at the end'); $this->assertEquals(['1', '2', '4', '5'], $row['body']['list3'], 'arrayRemove should remove the element'); } }