From eb7306a4fa7f4105d8afbe9dc39e6eae077e699c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 7 Oct 2025 22:17:59 +1300 Subject: [PATCH] Add missing state handlers --- src/Appwrite/Databases/TransactionState.php | 105 +++++++++++++++ .../Transactions/TransactionsBase.php | 123 ++++++++++++++++++ 2 files changed, 228 insertions(+) diff --git a/src/Appwrite/Databases/TransactionState.php b/src/Appwrite/Databases/TransactionState.php index 8df2bc657c..23dc6fc2e9 100644 --- a/src/Appwrite/Databases/TransactionState.php +++ b/src/Appwrite/Databases/TransactionState.php @@ -423,6 +423,33 @@ class TransactionState ]; break; + case 'increment': + case 'decrement': + $attribute = $data['attribute'] ?? null; + $value = $data['value'] ?? 1; + + if ($attribute) { + if (isset($state[$collectionId][$documentId])) { + $existingDocument = $state[$collectionId][$documentId]['document']; + $currentValue = $existingDocument->getAttribute($attribute, 0); + $newValue = $action === 'increment' ? $currentValue + $value : $currentValue - $value; + $existingDocument->setAttribute($attribute, $newValue); + + $currentAction = $state[$collectionId][$documentId]['action']; + if ($currentAction !== 'create' && $currentAction !== 'upsert') { + $state[$collectionId][$documentId]['action'] = 'update'; + } + } else { + $newValue = $action === 'increment' ? $value : -$value; + $state[$collectionId][$documentId] = [ + 'action' => 'update', + 'document' => new Document([$attribute => $newValue]), + 'exists' => true + ]; + } + } + break; + case 'bulkCreate': if (\is_array($data)) { foreach ($data as $doc) { @@ -437,6 +464,84 @@ class TransactionState } } break; + + case 'bulkUpdate': + if (isset($data['queries']) && isset($data['data'])) { + $queries = Query::parseQueries($data['queries'] ?? []); + $updateData = $data['data']; + + foreach ($state[$collectionId] ?? [] as $docId => $entry) { + if (!$entry['exists']) { + continue; + } + + $document = $entry['document']; + $filters = $this->extractFilters($queries); + + if ($this->documentMatchesFilters($document, $filters)) { + foreach ($updateData as $key => $value) { + if ($key !== '$id') { + $document->setAttribute($key, $value); + } + } + + $currentAction = $state[$collectionId][$docId]['action']; + if ($currentAction !== 'create' && $currentAction !== 'upsert') { + $state[$collectionId][$docId]['action'] = 'update'; + } + } + } + } + break; + + case 'bulkUpsert': + if (\is_array($data)) { + foreach ($data as $doc) { + if ($doc instanceof Document) { + $doc = $doc->getArrayCopy(); + } + + $docId = $doc['$id'] ?? null; + if (!$docId) { + continue; + } + + if (isset($state[$collectionId][$docId])) { + $existingDocument = $state[$collectionId][$docId]['document']; + foreach ($doc as $key => $value) { + $existingDocument->setAttribute($key, $value); + } + } else { + $state[$collectionId][$docId] = [ + 'action' => 'upsert', + 'document' => new Document($doc), + 'exists' => true + ]; + } + } + } + break; + + case 'bulkDelete': + if (isset($data['queries'])) { + $queries = Query::parseQueries($data['queries'] ?? []); + $filters = $this->extractFilters($queries); + + foreach ($state[$collectionId] ?? [] as $docId => $entry) { + if (!$entry['exists']) { + continue; + } + + $document = $entry['document']; + if ($this->documentMatchesFilters($document, $filters)) { + $state[$collectionId][$docId] = [ + 'action' => 'delete', + 'exists' => false + ]; + } + } + } + break; } } diff --git a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php index 1ea8d0cc9a..e50bb9f2bf 100644 --- a/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php +++ b/tests/e2e/Services/Databases/TablesDB/Transactions/TransactionsBase.php @@ -4613,4 +4613,127 @@ trait TransactionsBase $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'); + } + } }