Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/Database/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,6 @@ public function withTransaction(callable $callback): mixed
$action instanceof ConflictException ||
$action instanceof LimitException
) {
$this->inTransaction = 0;
throw $action;
}

Expand All @@ -439,7 +438,6 @@ public function withTransaction(callable $callback): mixed
continue;
}

$this->inTransaction = 0;
throw $action;
}
}
Expand Down
148 changes: 148 additions & 0 deletions tests/e2e/Adapter/Scopes/GeneralTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,154 @@ public function testTransactionAtomicity(): void
$database->deleteCollection('transactionAtomicity');
}

/**
* Test that withTransaction correctly resets inTransaction state
* when a known exception (DuplicateException) is thrown after successful rollback.
*/
public function testTransactionStateAfterKnownException(): void
{
/** @var Database $database */
$database = $this->getDatabase();

$database->createCollection('txKnownException');
$database->createAttribute('txKnownException', 'title', Database::VAR_STRING, 128, true);

$database->createDocument('txKnownException', new Document([
'$id' => 'existing_doc',
'$permissions' => [
Permission::read(Role::any()),
],
'title' => 'Original',
]));

// Trigger a DuplicateException inside withTransaction by inserting a duplicate ID
try {
$database->withTransaction(function () use ($database) {
$database->createDocument('txKnownException', new Document([
'$id' => 'existing_doc',
'$permissions' => [
Permission::read(Role::any()),
],
'title' => 'Duplicate',
]));
});
$this->fail('Expected DuplicateException was not thrown');
} catch (DuplicateException $e) {
// Expected
}

// inTransaction must be false after the exception
$this->assertFalse(
$database->getAdapter()->inTransaction(),
'Adapter should not be in transaction after DuplicateException'
);

// Database should still be functional
$doc = $database->getDocument('txKnownException', 'existing_doc');
$this->assertEquals('Original', $doc->getAttribute('title'));

$database->deleteCollection('txKnownException');
}

/**
* Test that withTransaction correctly resets inTransaction state
* when retries are exhausted for a generic exception.
*/
public function testTransactionStateAfterRetriesExhausted(): void
{
/** @var Database $database */
$database = $this->getDatabase();

$attempts = 0;

try {
$database->withTransaction(function () use (&$attempts) {
$attempts++;
throw new \RuntimeException('Persistent failure');
});
$this->fail('Expected RuntimeException was not thrown');
} catch (\RuntimeException $e) {
$this->assertEquals('Persistent failure', $e->getMessage());
}

// Should have attempted 3 times (initial + 2 retries)
$this->assertEquals(3, $attempts, 'Should have exhausted all retry attempts');

// inTransaction must be false after retries exhausted
$this->assertFalse(
$database->getAdapter()->inTransaction(),
'Adapter should not be in transaction after retries exhausted'
);
}

/**
* Test that nested withTransaction calls maintain correct inTransaction state
* when the inner transaction throws a known exception.
*/
public function testNestedTransactionState(): void
{
/** @var Database $database */
$database = $this->getDatabase();

$database->createCollection('txNested');
$database->createAttribute('txNested', 'title', Database::VAR_STRING, 128, true);

$database->createDocument('txNested', new Document([
'$id' => 'nested_existing',
'$permissions' => [
Permission::read(Role::any()),
],
'title' => 'Original',
]));

// Outer transaction should succeed even if inner transaction throws
$result = $database->withTransaction(function () use ($database) {
$database->createDocument('txNested', new Document([
'$id' => 'outer_doc',
'$permissions' => [
Permission::read(Role::any()),
],
'title' => 'Outer',
]));

// Inner transaction throws a DuplicateException
try {
$database->withTransaction(function () use ($database) {
$database->createDocument('txNested', new Document([
'$id' => 'nested_existing',
'$permissions' => [
Permission::read(Role::any()),
],
'title' => 'Duplicate',
]));
});
} catch (DuplicateException $e) {
// Caught and handled — outer transaction should continue
}

return true;
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

$this->assertTrue($result);

// inTransaction must be false after everything completes
$this->assertFalse(
$database->getAdapter()->inTransaction(),
'Adapter should not be in transaction after nested transactions complete'
);

// Outer document should have been committed
$outerDoc = $database->getDocument('txNested', 'outer_doc');
$this->assertFalse($outerDoc->isEmpty(), 'Outer transaction document should exist');
$this->assertEquals('Outer', $outerDoc->getAttribute('title'));

// Original document should be unchanged
$existingDoc = $database->getDocument('txNested', 'nested_existing');
$this->assertEquals('Original', $existingDoc->getAttribute('title'));

$database->deleteCollection('txNested');
}

/**
* Wait for Redis to be ready with a readiness probe
*/
Expand Down
Loading