Skip to content

Keep the user transaction active after a failed statement - #536

Merged
JanJakes merged 5 commits into
WordPress:trunkfrom
Nandan108:fix/keep-transaction-after-failed-statement
Oct 27, 2026
Merged

JanJakes merged 5 commits into
WordPress:trunkfrom
Nandan108:fix/keep-transaction-after-failed-statement

Conversation

@Nandan108

@Nandan108 Nandan108 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Part of #473 (the "Explicit transactions" section).

When a statement fails inside a transaction, the driver rolls back the whole transaction and ends it. MySQL undoes only the failed statement and keeps the transaction active:

CREATE TABLE t (id INT PRIMARY KEY);
START TRANSACTION;
INSERT INTO t VALUES (1);
INSERT INTO t VALUES (1);  -- fails with a duplicate key error
INSERT INTO t VALUES (2);
COMMIT;
SELECT id FROM t;
-- Driver: 2     MySQL: 1, 2

The work before the error is lost, and the work after it runs in autocommit mode. A savepoint created before the error is gone too, so ROLLBACK TO SAVEPOINT fails with error 1305.

Fix

Each statement already runs in a wrapper: a transaction when none is active, or a savepoint inside a user transaction. On failure, only that wrapper is now rolled back. This:

  • keeps the user transaction and its savepoints active, and still undoes all changes of the failed statement, including the earlier rows of a multi-row INSERT;
  • keeps the transaction opened by LOCK TABLES, so UNLOCK TABLES commits the rows written before the error, as in MySQL;
  • replaces the special case for error 1305 (missing savepoint), since savepoint statements are not wrapped and have nothing to roll back.

Without a user transaction, the behavior is unchanged: the wrapper transaction is rolled back.

Some SQLite errors roll back the whole transaction automatically (SQLite docs). Then rolling back to the savepoint fails, and the driver ends the transaction as before. This path has no test, because those errors (such as SQLITE_FULL) are hard to trigger in a unit test.

The rest of #473 (error modes and diagnostics) is not part of this PR.

Tests

  • testFailedStatementKeepsTransactionActive checks that a failed write and a failed read keep the transaction active, and that COMMIT keeps the work before and after them.
  • testFailedStatementIsRolledBackInTransaction checks that a failed multi-row INSERT leaves none of its rows.
  • testRollbackToSavepointAfterFailedStatement checks that a savepoint survives a failed statement.
  • testFailedStatementKeepsTableLock checks that UNLOCK TABLES keeps the rows written before a failed statement.
  • test_failed_query_keeps_transaction_active checks the same through PDO::beginTransaction() and PDO::commit().

All five fail on trunk. The existing testMissingSavepointDoesNotRollbackTransaction still passes.

packages/mysql-on-sqlite: 1048 tests pass (PHP 8.3, SQLite 3.45.1); PHPCS is clean.

Use of AI

This change was prepared with Claude Code (Claude Opus 5.5). I reviewed the code, the tests, and this description, and I take responsibility for the change.

Note: this PR and #535 both add tests at the same spot of WP_MySQL_On_SQLite_PDO_API_Tests.php, so whichever merges second needs a trivial rebase.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Failed statements preserve an active transaction, allowing you to continue working and commit later.
    • Changes from a failed statement are rolled back without discarding earlier work in the transaction.
    • Savepoints remain usable after statement failures, and table locks remain active unless SQLite rolls back the transaction.
    • Failed commits roll back transaction changes and release table locks. Transaction state is also cleared when SQLite ends a transaction after an error.

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: dc1a973d-700e-4644-9f1a-ec94cb81aed0
Commits

Reviewing files that changed from the base of the PR and between a07ac22 and 771c562.

Files selected for processing: 1
  • packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

Walkthrough

The SQLite wrapper now handles failed queries, commits, and rollbacks with updated transaction and savepoint state. Regression tests cover statement failures, failed commits, savepoints, and table locks.

Changes: Transaction failure handling

Layer / File(s) Summary
Query failure rollback
packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php, packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php
Failed queries roll back the wrapper transaction. If the user transaction remains active, its state and table lock are preserved. Tests cover statement failures, partial changes, savepoints, and table locks.
Commit failure handling
packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php, packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_PDO_API_Tests.php, packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php
PDO commit executes SQLite COMMIT directly and clears state after success. MySQL commit attempts rollback after a SQLite commit error and rethrows the original error. Tests cover failed commits through explicit and implicit commit paths.
Rollback state cleanup
packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php, packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php
User rollback clears transaction and savepoint state in a finally block. Tests check the state after SQLite rolls back the full transaction and releases a table lock.

Priority: Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Possibly related PRs

Suggested reviewers: janjakes, chubes4

Merge Risk: Minimal · up to 771c5

No actionable merge-blocking issue is identified; the change appears mergeable after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surv… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title "Keep the user transaction active after a failed statement" matches the main change in the changeset. The raw summary and PR objectives both describe rolling back only the statement's wrappe…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title "Keep the user transaction active after a failed statement" matches the main change in the changeset. The raw summary and PR objectives both describe rolling back only the statement's wrapper savepoint so the user transaction and its savepoints stay active after a failed query. The title is short, specific, and free of file lists or vague wording. The title does not mention the failed-commit handling or the table-lock preservation, but it covers the primary change.

Full details: Docstring Coverage

Explanation

Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 15 functions across 3 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

PHPStan (2.3.0)

PHPStan reported 1 non-file analysis error(s); analysis is incomplete

PHPMD (3.0.0)
packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php

PHPMD could not process this file (exit code 255): 0/1 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20971520 bytes) in phar:///usr/bin/phpmd/vendor/pdepend/pdepend/src/Util/Cache/Driver/FileCacheDriver.php on line 183

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.

When a statement failed, the driver rolled back the whole user
transaction, including the work done before the failed statement, and
ended it. MySQL undoes only the failed statement and keeps the
transaction active, so the caller can decide whether to commit or roll
back the rest. A later COMMIT kept only the work done after the error,
and a savepoint created before the error could no longer be used.

Roll back only the wrapper transaction of the failed statement. Inside a
user transaction, the wrapper is a savepoint, so the transaction stays
active. This also keeps an active LOCK TABLES transaction, and replaces
the special case for a missing savepoint, since savepoint statements are
not wrapped.

Part of WordPress#473.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JanJakes

Copy link
Copy Markdown
Member

Thanks, @Nandan108! I pushed a few commits on top: a failed COMMIT now rolls back instead of leaving the transaction open, the rollback test uses ALTER TABLE since SQLite already undoes a failed INSERT by itself, and the rollback code is simpler, with tests for SQLite rolling back the whole transaction.

I also extracted one more issue to address as a follow-up: #576

SQLite undoes a failed INSERT by itself, so the multi-row INSERT test
passed even without the savepoint rollback.

A multi-table DELETE runs a DELETE query for each table. When deleting
from the second table fails, only the savepoint rollback restores the
rows deleted from the first one.

ALTER TABLE also runs multiple queries, but MySQL commits the active
transaction before DDL statements, so the transaction would not stay
active after a failed ALTER TABLE.
Some SQLite errors, such as SQLITE_FULL, roll back the whole transaction,
including the wrapper savepoint. A RAISE(ROLLBACK) trigger makes this
path testable without such errors.

Check that the transaction, its savepoints, and its table lock end, and
that the connection stays usable.
Reuse rollback_user_transaction() to end the transaction, both for a
transaction wrapper and when SQLite has already rolled back the whole
transaction. It now always resets the transaction state, as ROLLBACK
always ends the transaction, even when it fails.

End the user transaction only when ROLLBACK TO fails. When only RELEASE
fails, the statement is already undone, and the leftover savepoint is
harmless.

Release the table lock outside of the try block, as the rollback can now
throw on PHP < 8.4. Don't reset the wrapper transaction type, as flush()
already does that before every query.
Failed statements no longer roll back the user transaction, and COMMIT
runs without a wrapper transaction. So when COMMIT failed, nothing was
rolled back and the transaction stayed open. It kept the write lock, and
later statements silently joined the uncommitted transaction.

Like MySQL, roll back a transaction that fails to commit. This covers
COMMIT, UNLOCK TABLES, and the implicit commits of START TRANSACTION,
BEGIN, and LOCK TABLES.

PDO::commit() still keeps the transaction active on failure. PDO MySQL
still reports an active transaction after a failed commit, so callers
can call PDO::rollBack().

The tests make COMMIT fail with a deferred foreign key check.
@JanJakes
JanJakes force-pushed the fix/keep-transaction-after-failed-statement branch from a07ac22 to 771c562 Compare October 27, 2026 16:11
@JanJakes
JanJakes merged commit c7d6963 into WordPress:trunk Oct 27, 2026
17 checks passed
@JanJakes JanJakes mentioned this pull request Oct 28, 2026
JanJakes added a commit that referenced this pull request Oct 28, 2026
## Release `3.1.2`

Version bump and changelog update for release `3.1.2`.

**Changelog draft:**
* Allow configuring the SQLite busy timeout with `SQLITE_BUSY_TIMEOUT`
([#558](#558))
* Keep the user transaction active after a failed statement
([#536](#536))
* Support odd-length `0x` hexadecimal literals
([#573](#573))
* Fix rebuilds of legacy text and binary columns
([#565](#565))
* Use `'0'` column defaults in non-strict `INSERT` statements
([#569](#569))
* Roll back failed `OPTIMIZE TABLE` and `REPAIR TABLE` rebuilds
([#567](#567))
* Remove the Rust parser extension
([#566](#566))
* Preserve `AUTO_INCREMENT` counters when rebuilding tables
([#561](#561))
* Fix `ALTER TABLE ... ADD (...)` for columns, indexes, and constraints
([#557](#557))
* Fix `ON UPDATE CURRENT_TIMESTAMP` for explicit values and unchanged
rows
([#560](#560))
* Normalize zero defaults in date and time columns
([#542](#542))
* Add support for `ALTER COLUMN` with `SET DEFAULT` and `DROP DEFAULT`
([#555](#555))

**Full changelog:**
v3.1.1...release/v3.1.2

## Next steps

1. **Review** the changes in this pull request.
2. **Push** any additional edits to this branch (`release/v3.1.2`).
3. **Merge** this pull request to complete the release.

Merging will automatically build the plugin ZIP, create a [GitHub
release](https://github.com/WordPress/sqlite-database-integration/releases),
and deploy to
[WordPress.org](https://wordpress.org/plugins/sqlite-database-integration/).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Release**
  * Updated the SQLite database integration to version 3.1.2.

* **New Features**
* Added support for more `ALTER TABLE` operations and configurable
SQLite busy timeouts.

* **Bug Fixes**
* Improved transaction handling after failed statements and addressed
SQL compatibility, table rebuild, and default-value handling issues.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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