fix: open real transactions when no boundary exists - #3438
Merged
Conversation
TransactionScope forced the savepoint path on SQLite and treated an unsupported @@autocommit as autocommit disabled, so it issued a bare SAVEPOINT with no enclosing transaction. Engines that do not implement @@autocommit report NULL, which casts to 0 and was read as "a caller owns the outer transaction". Only a non-NULL zero now counts, and the driver-specific shortcut is gone, so a scope opens a real transaction when nothing encloses it. A bare SAVEPOINT is invalid on MySQL: it opens no transaction, and releasing it fails with error 1305. Upstream sqlite-database-integration is aligning with those semantics, which turns the old path into failed commits and rollbacks that silently leave writes applied. Nesting can no longer be inferred from server variables, because a plain START TRANSACTION is invisible to them on SQLite and a second one is an implicit commit of the first. Scope depth is tracked in process instead, and unresolved scopes release their depth so later scopes still open a real boundary. Refs #3436
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3436
Problem
TransactionScope::begin()issued a bareSAVEPOINTon SQLite with no enclosing transaction, then relied onRELEASE SAVEPOINTto commit it. That sequence is invalid on MySQL and is becoming invalid on SQLite.Verified on MariaDB 10.11.14 with autocommit enabled:
WordPress/sqlite-database-integration#496 aligns SQLite with those semantics. Against its head (
3cc53c7), our exact sequence fails:RELEASE SAVEPOINTreturns 1305 socommit()reports failure, and on the rollback path bothROLLBACK TO SAVEPOINTandRELEASE SAVEPOINTfail while the write stays applied.Why the obvious fix was not enough
Three findings changed the shape of this patch. Details and probe output in the issue comment.
$in_transaction = is_sqlite(), which already skipped the probe. The|| is_sqlite()on line 38 was redundant, so removing only it would have fixed nothing.@@autocommitis NULL on SQLite. The adapter does not implement it.(int) null === 0, and the check was0 === (int) ..., so even with both special-cases removed the probe still concluded "caller owns a transaction" and still opened a bare savepoint.START TRANSACTIONis worse. A plainSTART TRANSACTIONis invisible to every server variable on SQLite, and a second one implicitly commits the first. Measured: outer write survived an inner rollback. That silently converts a nested rollback into an early commit.Change
BaseRepository::is_sqlite()special-cases.@@autocommitas an enclosing transaction.Savepoints are still used for genuinely nested scopes. Commit, rollback, ownership, and retry behavior are unchanged.
Verification
Patched code, four scenarios, identical on MariaDB 10.11.14 and SQLite at both
3cc53c7(PR #496 head) and48c457e:committedBaseline against
3cc53c7fails all four (commit: false,rolled_back,inner,inner2).Without the destructor, an abandoned scope causes the next independent scope to emit a bare savepoint again; with it, that scope correctly opens
START TRANSACTION.WordPress test isolation is preserved: the test framework issues
SET autocommit = 0, which the adapter does persist, so tests continue to take the savepoint path.Tests
Added coverage that fails on the current code:
test_absent_autocommit_variable_is_not_read_as_open_transaction— the core regression; fails on baseline, which emitsSAVEPOINTinstead ofSTART TRANSACTION.test_disabled_autocommit_opens_savepoint_scope/test_enabled_autocommit_opens_transaction_scope— pin both probe branches.test_abandoned_scope_does_not_strand_later_scopes— depth release.test_inner_scope_rollback_preserves_outer_scope_write/test_outer_scope_rollback_discards_committed_inner_scope— nesting guards.The autocommit-branch tests use a small recording double because the live test connection always runs inside the WordPress test transaction, which makes the autocommit-enabled branch unreachable through
$GLOBALS['wpdb'].AI assistance