Skip to content

Fix transaction state management across adapters#817

Merged
abnegate merged 6 commits intomainfrom
fix-transaction-state-reset
Feb 18, 2026
Merged

Fix transaction state management across adapters#817
abnegate merged 6 commits intomainfrom
fix-transaction-state-reset

Conversation

@premtsd-code
Copy link
Contributor

@premtsd-code premtsd-code commented Feb 17, 2026

Summary

  • Remove redundant $this->inTransaction = 0 resets in base Adapter::withTransaction() after successful rollback — all adapter implementations already handle the counter correctly
  • Fix Mongo withTransaction() to bypass transaction management for nested calls, preventing state corruption of the outer transaction's counter and session
  • Add missing $this->inTransaction = 0 reset in Postgres rollbackTransaction() on PDOException, matching SQL adapter behavior
  • Add e2e tests covering transaction state after known exceptions, retry exhaustion, and nested transactions

Test plan

  • testTransactionStateAfterKnownException — verifies inTransaction() is false after a DuplicateException
  • testTransactionStateAfterRetriesExhausted — verifies state after all retries are exhausted (skipped for MongoDB which has no retry logic)
  • testNestedTransactionState — verifies outer transaction commits when inner transaction throws (skipped for MongoDB which doesn't support nested transactions)

Summary by CodeRabbit

  • New Features

    • Enhanced transaction management with improved support for nested transactions across database adapters.
    • Added automatic transaction retry capability detection.
  • Bug Fixes

    • Improved transaction state recovery during exception handling and exhausted retries.
  • Tests

    • Expanded test coverage for transaction state validation in error scenarios.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

Warning

Rate limit exceeded

@premtsd-code has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 21 minutes and 19 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adjusted transaction handling: removed two resets of the inTransaction counter in Adapter::withTransaction, added capability queries for transaction retries and nested transactions, implemented nested savepoint handling in Postgres, guarded Mongo against nested transactions and non-replica sets, and added e2e tests for these behaviors.

Changes

Cohort / File(s) Summary
Adapter core
src/Database/Adapter.php
Removed resets of inTransaction in two withTransaction failure paths; added abstract capability methods getSupportForTransactionRetries() and getSupportForNestedTransactions() with docblocks.
Mongo adapter
src/Database/Adapter/Mongo.php
Bypass nested transactions when inTransaction > 0; return early for non-replica-set cases in withTransaction; implement getSupportForTransactionRetries() and getSupportForNestedTransactions() returning false.
Postgres adapter
src/Database/Adapter/Postgres.php
Introduce nested-transaction handling via SAVEPOINTs when inTransaction > 0; rollbackTransaction rolls back to savepoint for nested transactions and resets inTransaction on exceptional rollback paths.
SQL & Pool adapters
src/Database/Adapter/SQL.php, src/Database/Adapter/Pool.php
Added getSupportForTransactionRetries() and getSupportForNestedTransactions() methods; SQL returns true for both; Pool delegates capability queries to its delegate.
Tests (e2e)
tests/e2e/Adapter/Scopes/GeneralTests.php
Added tests: testTransactionStateAfterKnownException, testTransactionStateAfterRetriesExhausted, and testNestedTransactionState (duplicated blocks appear in diff).

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant Adapter
    participant PostgresDB
    participant MongoDB

    Caller->>Adapter: withTransaction(callback)
    alt Adapter supports nested and inTransaction > 0 (Postgres)
        Adapter->>PostgresDB: SAVEPOINT transaction{n}
        Adapter->>Adapter: inTransaction++
        Caller->>Adapter: execute callback
        alt callback commits
            Adapter->>PostgresDB: RELEASE SAVEPOINT / commit
            Adapter->>Adapter: inTransaction--
        else callback rolls back
            Adapter->>PostgresDB: ROLLBACK TO SAVEPOINT transaction{n-1}
            Adapter->>Adapter: inTransaction--
        end
    else Adapter is Mongo and inTransaction > 0
        Adapter->>MongoDB: execute callback directly (no nested support)
    else Adapter not replica set (Mongo)
        Adapter->>Caller: execute callback directly
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • fogelito
  • abnegate

Poem

🐇 I hop through code and mark each nest,

Savepoints snug where branches rest.
Known bumps I skip, retries I count,
Tests keep tunnels tidy—bounce by bounce.
A carrot, a commit — transactions blessed.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and directly summarizes the main change: fixing transaction state management across database adapters, which is the primary objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-transaction-state-reset

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/e2e/Adapter/Scopes/GeneralTests.php (2)

857-858: Consider using finally blocks for test cleanup to avoid orphaned collections.

Both testTransactionStateAfterKnownException and testNestedTransactionState create collections and clean them up at the end, but if an assertion fails mid-test, deleteCollection is never reached. Wrapping cleanup in finally (or using setUp/tearDown) would prevent leftover collections from affecting subsequent test runs.

Also applies to: 937-938

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/Adapter/Scopes/GeneralTests.php` around lines 857 - 858, Tests
testTransactionStateAfterKnownException and testNestedTransactionState create
collections via Database::createCollection and remove them with
Database::deleteCollection but do not guarantee cleanup if assertions fail; wrap
the create/delete logic in a try/finally or move cleanup into the test class
tearDown to ensure deleteCollection always runs, i.e., perform createCollection
before the try, run assertions inside try, and call deleteCollection inside
finally (or register collection names in setUp and remove them in tearDown) so
orphaned collections cannot remain after failures.

918-919: Hardcoded retry count couples test to internal implementation.

The expected 3 attempts is hardcoded at line 404 of src/Database/Adapter.php as $retries = 2 (1 initial + 2 retries). If that default changes, this test fails without clear cause. Consider extracting the retry count to a class constant in Adapter and reference it in the test, or add a comment documenting the dependency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/Adapter/Scopes/GeneralTests.php` around lines 918 - 919, The test
is hardcoded to expect 3 attempts which couples it to Adapter's internal
$retries value; add a public class constant (e.g., Adapter::DEFAULT_RETRIES) or
a public static getter (e.g., Adapter::getDefaultRetries()) in the Adapter class
where $retries is defined, set it to the current default (2), and update the
test in GeneralTests.php to compute expected attempts as
Adapter::DEFAULT_RETRIES + 1 (initial attempt + retries) instead of the literal
3 so the test follows the implementation default.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/e2e/Adapter/Scopes/GeneralTests.php`:
- Around line 948-974: The Mongo adapter's withTransaction implementation
mishandles nested transactions by resetting $this->inTransaction to 0 in the
inner transaction's finally/cleanup path; update Mongo.php so that
withTransaction decrements $this->inTransaction (e.g., --$this->inTransaction)
rather than assigning 0 when leaving a nested transaction, ensuring
commitTransaction sees the correct nesting level and outer transactions can
commit; alternatively, if Mongo replica-set transactions cannot support nesting,
add a test skip for this case, but the preferred fix is to correct the
inTransaction counter in the withTransaction method and ensure
commitTransaction/abortTransaction rely on that counter.

---

Nitpick comments:
In `@tests/e2e/Adapter/Scopes/GeneralTests.php`:
- Around line 857-858: Tests testTransactionStateAfterKnownException and
testNestedTransactionState create collections via Database::createCollection and
remove them with Database::deleteCollection but do not guarantee cleanup if
assertions fail; wrap the create/delete logic in a try/finally or move cleanup
into the test class tearDown to ensure deleteCollection always runs, i.e.,
perform createCollection before the try, run assertions inside try, and call
deleteCollection inside finally (or register collection names in setUp and
remove them in tearDown) so orphaned collections cannot remain after failures.
- Around line 918-919: The test is hardcoded to expect 3 attempts which couples
it to Adapter's internal $retries value; add a public class constant (e.g.,
Adapter::DEFAULT_RETRIES) or a public static getter (e.g.,
Adapter::getDefaultRetries()) in the Adapter class where $retries is defined,
set it to the current default (2), and update the test in GeneralTests.php to
compute expected attempts as Adapter::DEFAULT_RETRIES + 1 (initial attempt +
retries) instead of the literal 3 so the test follows the implementation
default.

@premtsd-code premtsd-code changed the title Remove redundant inTransaction reset after successful rollback Fix transaction state management across adapters Feb 17, 2026
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid checks like "is this an x" and instead use a "does this support x" pattern. So here we should use a (new) method getSupportForTransactionRetries instead of explicitly checking for Mongo.

This way, adding the adapters themselves are the source of truth, so we would never add to add another condition here which might not be the case with instance checks.

@premtsd-code premtsd-code force-pushed the fix-transaction-state-reset branch from 88bb8c5 to 19c129d Compare February 18, 2026 07:01
@premtsd-code premtsd-code force-pushed the fix-transaction-state-reset branch from 19c129d to 143e3bb Compare February 18, 2026 07:06
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Database/Adapter/SQL.php`:
- Around line 3580-3589: Remove the trailing whitespace on the blank line
between the methods getSupportForTransactionRetries() and
getSupportForNestedTransactions() in class SQL (the blank line after
getSupportForTransactionRetries returns) so the file no longer triggers the
PSR-12 `no_whitespace_in_blank_line` Pint lint error; after removing the
trailing spaces, save and re-run the linter to confirm the violation is
resolved.

Comment on lines 3580 to 3589

public function getSupportForTransactionRetries(): bool
{
return true;
}

public function getSupportForNestedTransactions(): bool
{
return true;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix trailing whitespace on line 3585 to resolve the Pint lint failure.

The CI pipeline reports a no_whitespace_in_blank_line PSR-12 violation. Line 3585 has trailing whitespace on the blank line between the two methods.

🧹 Fix trailing whitespace
     public function getSupportForTransactionRetries(): bool
     {
         return true;
     }
-    
+
     public function getSupportForNestedTransactions(): bool
     {
         return true;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function getSupportForTransactionRetries(): bool
{
return true;
}
public function getSupportForNestedTransactions(): bool
{
return true;
}
public function getSupportForTransactionRetries(): bool
{
return true;
}
public function getSupportForNestedTransactions(): bool
{
return true;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Adapter/SQL.php` around lines 3580 - 3589, Remove the trailing
whitespace on the blank line between the methods
getSupportForTransactionRetries() and getSupportForNestedTransactions() in class
SQL (the blank line after getSupportForTransactionRetries returns) so the file
no longer triggers the PSR-12 `no_whitespace_in_blank_line` Pint lint error;
after removing the trailing spaces, save and re-run the linter to confirm the
violation is resolved.

@abnegate abnegate merged commit e3305f6 into main Feb 18, 2026
18 checks passed
@abnegate abnegate deleted the fix-transaction-state-reset branch February 18, 2026 07:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments