ext/pdo: Add PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS attribute#21257
ext/pdo: Add PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS attribute#21257Crovitche-1623 wants to merge 1 commit intophp:masterfrom
Conversation
When autocommit is off, MySQL's SERVER_STATUS_IN_TRANS flag is cleared after COMMIT/ROLLBACK even though the session is still logically in a transaction. This causes commit() and rollBack() to throw "There is no active transaction" in the gap before the next SQL statement. This new opt-in attribute makes commit() and rollBack() return true (no-op) instead of throwing when autocommit is off and no server-level transaction is active. This eliminates the need for the redundant BEGIN that frameworks send after every COMMIT to work around this gap.
|
Thank you for the very detailed description of the PR and the problem, however I don't think I fully understand the reasoning behind it. To summarise, it seems to me like you would like to introduce a flag in PDO to bring back the silent behaviour that was present in PHP 7 when PDO didn't report the error "There is no active transaction". But I am not sure why? You gave 5 examples of frameworks where the new behaviour was noticed, but then you said that the last 4 are irrelevant to what you are talking about. The one that is relevant doesn't have a linked PR or bug report. This makes it more difficult to buy into the problem. You claim that people now have to add
What gap? Why would you want to call This code throws an exception and so does this one: What is the difference? Where is the gap? If there is no transaction, then there is nothing to commit. |
|
The issue is that with However, Your second example calls The real scenario is: $pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '', [
PDO::ATTR_AUTOCOMMIT => false,
]);
$pdo->exec('INSERT INTO t VALUES (1)');
$pdo->commit(); // OK — flag was set by the INSERT
$pdo->exec('INSERT INTO t VALUES (2)');
$pdo->commit(); // OK — flag was set by the INSERT
$pdo->commit(); // Throws — but MySQL is still logically in a transactionThe third Frameworks like Doctrine DBAL that use |
The "gap" is the window between |
|
Regarding the issues I mentioned, you're right. I searched for the error message |
That's not true. It's only true from the perspective of a developer, but not from the perspective of the server. The transaction is started only when needed, i.e. when a non-DDL query is sent. The flag represents the true state of the transaction. That's why the PHP 8 behaviour is the correct one now.
But it's not in a new implicit transaction yet. Your example is exactly the same as mine. My point still stands: calling In my example above, I was trying to show you that after opening a connection with autocommit off there is no transaction yet. A
Yeah, but when is MySQL not ready to accept None of these will report any error from MySQL because MySQL doesn't care about noop
Ahh, I see. So it's just a logical problem. But that's exactly how it should be. It's the framework's choice to issue a manual Think of transactions in a less opaque way:
So the only difference between autocommit off and on is that when it's off, you must trigger commit manually. |
You're right. The point I was trying to make is that with
That's true, but from my POV only if Moreover, I think maybe we're conflating two separate issues. My PR is not about DDL implicit commits — it's specifically about the gap after an explicit
They do have to. The only alternative is to defer the BEGIN until the next actual query is about to be sent — which means the framework must intercept every query to inject a lazy BEGIN before it. That's significantly more complex than just sending BEGIN immediately after COMMIT, which is why frameworks take the simpler approach — at the cost of a redundant round-trip. In my opinion, the |
|
In other words, you prefer having visibility on the implicit commit problem through a flag that doesn't fully reflect MySQL's actual state, rather than relying on whether MySQL can accept the COMMIT or not. Is that correct? |
It will also accept it at any time with autocommit = 1, as I have shown in my previous reply.
It's not supposed to. The flag is only there to indicate whether the transaction is pending. If no changes were made to the data, then there is no transaction. You can call
Yes, it was a bit confusing at first, but I think I understand what you are talking about. I was just disputing that there is a gap. In my opinion, there is no gap.
Then I probably don't understand the full picture here. When you are in autocommit=off mode, there is no need to call What I imagine the problem is, is that the framework is trying to indicate to its user that they don't need to start a transaction because autocommit is off. However, until the transaction is started, the |
The flag reflects MySQL's true state. After all, it comes from the MySQL server. It's got nothing to do with whether MySQL can accept |
Exactly ! My problem was that after a Frameworks cannot guarantee that a statement will always be executed before the next |
|
Can't you just do: |
It would solve the use case of the conditional data because Unfortunately, I cannot use this in my situation since I'm using Doctrine DBAL. They tried to add |
|
However, even if |
|
This discussion has been very productive, but unfortunately, I am going to decline the addition of The issues you are describing require different solutions. And it's going to be mostly on the framework developers for not being able to provide you with the full capabilities of PDO. The proposal you linked to, about adding similar functions to the legacy drivers, could be good but there doesn't seem to be much interest in this. Such an RFC would probably pass if someone were interested in doing it. But after that it would also require DBAL to implement it too. If I recall correctly, in DBAL you can still get the pure PDO object, and so you can call As you said, using For example, you could do something similar to what they do in EasyDB: https://github.com/paragonie/easydb/blob/39ba24863954af16351c4ae2150d2fb7c66c23f6/src/EasyDB.php#L1315 Either way, adding See also my answer on Stack Overflow from 2 years ago https://stackoverflow.com/a/79090764/1839439 |
Short description
Proposal for a new opt-in PDO attribute that makes
commit()androllBack()aware of the logical transaction state when autocommit is disabled, eliminating the need for a redundantBEGINafter everyCOMMIT.Problem statement
When
PDO::ATTR_AUTOCOMMITis set tofalse, MySQL sendsSET autocommit = 0at connection time. In this mode, every SQL statement is implicitly part of a transaction — there is no "auto-commit after each statement" behavior. ACOMMITends the current transaction, and the next statement implicitly starts a new one.However, MySQL's
SERVER_STATUS_IN_TRANSflag — which PHP 8+'sPDO::inTransaction()andPDO::commit()rely on — is cleared byCOMMIT. It is only set again when the next SQL statement executes (any statement, includingSELECT). This creates a gap:During this gap (between steps 3 and 4), calling
PDO::commit()throws:This forces every framework and ORM to implement the same workaround: immediately send
BEGINafter everyCOMMITto re-set the server flag. ThisBEGINis functionally redundant — MySQL is already ready for an implicit transaction — but without it, the nextcommit()call fails.Concrete impact
Each level-1 commit produces two SQL statements instead of one:
In a typical web request with 1–3 explicit commits, this means 1–3 wasted round-trips to MySQL per request.
Benchmark results
A benchmark comparing
COMMIT + BEGIN(current workaround) vsCOMMITonly (proposed behavior) on MySQL 8.4 (Docker, local network). Results averaged over 2 runs of 50,000 commit cycles each (100,000 total cycles):The redundant
BEGINadds ~12% overhead and ~73 µs per commit. This is on a local Docker network with near-zero latency — in production environments with network latency between application and database (cloud services, separate hosts), the per-commit cost would be higher.Benchmark script
Context: PHP 7 → PHP 8 behavioral change
PHP 7
PDO::inTransaction()andPDO::commit()checked an internalin_txnboolean, managed entirely client-side:beginTransaction()→in_txn = truecommit()/rollBack()→in_txn = falseThis flag was purely based on PDO API calls and did not reflect the actual server state. For example, a DDL statement (
CREATE TABLE) triggers an implicit commit in MySQL, but PHP 7'sinTransaction()still returnedtrue.PHP 8+
The MySQL PDO driver now provides
pdo_mysql_in_transaction()inext/pdo_mysql/mysql_driver.c, which queries the actual server status:Both
PDO::inTransaction()andPDO::commit()delegate to the same internal helperpdo_is_in_transaction():This change was a net improvement — it correctly detects implicit commits from DDL statements. But it introduced the gap problem described above for users with
autocommit = 0, becauseSERVER_STATUS_IN_TRANSis cleared afterCOMMITeven when autocommit is off.Affected projects
Multiple major PHP frameworks encountered the broader "no active transaction" problem on PHP 8+ and implemented workarounds. The root trigger varies:
Connectionsubclassesautocommit = 0gapBEGINafter everyCOMMIT(in custom wrapper)inTransaction()beforecommit()/rollBack()inTransaction()beforerollBack()commit()/rollBack()callsImportant distinction: the Drupal, Laravel, Yii2, and Doctrine Migrations issues are triggered by DDL statements (
CREATE TABLE,ALTER TABLE) that cause MySQL to implicitly commit the current transaction — this happens regardless of the autocommit setting. The Doctrine DBAL workaround (customConnectionsubclass sendingBEGINafter everyCOMMIT) is the one directly caused by theautocommit = 0gap.This proposal specifically targets the
autocommit = 0gap. However, it also helps the DDL case when autocommit is off: after a DDL implicit commit,SERVER_STATUS_IN_TRANSis cleared, andcommit()/rollBack()would throw — the attribute prevents this throw.For the DDL case with autocommit on, a different solution would be needed (e.g., checking
inTransaction()before callingcommit(), as Drupal does).Proposed solution
New attribute:
PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONSA new opt-in PDO attribute that modifies the behavior of
commit()androllBack()when autocommit is disabled and no server-level transaction is active.Behavioral change (only when both conditions are met)
When
ATTR_AUTOCOMMIT_AWARE_TRANSACTIONSistrueandATTR_AUTOCOMMITisfalse:commit()truesilently (no-op)rollBack()truesilently (no-op)inTransaction()falsefalsebeginTransaction()Key design decisions:
inTransaction()is unchanged — it continues to reflect the actualSERVER_STATUS_IN_TRANSflag. This preserves backward compatibility and keepsinTransaction()accurate for code that relies on the server-level transaction state.beginTransaction()is unchanged — it still works in the gap, which is correct because MySQL can accept an explicitBEGINat any time when autocommit is off.Only
commit()androllBack()are affected — they silently succeed instead of throwing when there is nothing to commit/rollback. This is safe because aCOMMITorROLLBACKwith no pending changes is a no-op from MySQL's perspective.Scope
autocommit = 0+SERVER_STATUS_IN_TRANSgap is the most common pain pointWhy this matters
Before (current state)
Every framework with
autocommit = 0must send a redundantBEGINafter everyCOMMIT:After (with the attribute)
The redundant
BEGINis eliminated — each commit cycle costs one round-trip instead of two. For applications with frequent commits, this is a measurable improvement in latency and MySQL thread utilization.Cross-driver compatibility
The proposed change is in core PDO (
pdo_dbh.c), not in a specific driver. The guard condition!dbh->auto_commitensures that the new behavior only activates when a driver actually supports disabling autocommit.ATTR_AUTOCOMMITsupportin_transaction()SERVER_STATUS_IN_TRANS)BEGINPQtransactionStatus)auto_commitstaystruesqlite3_get_autocommit)auto_commitstaystruein_txn)auto_commitstaystruein_txn)in_txnin_manually_txn)in_txn)in_txnKey observations:
PostgreSQL, SQLite, SQL Server do not support
ATTR_AUTOCOMMIT = false. Theauto_commitfield stays at its defaulttruevalue — the proposed guard condition is never satisfied. Zero impact.Oracle, ODBC support
ATTR_AUTOCOMMITbut rely on PDO's internalin_txnflag (no driver-levelin_transaction()). There is no gap: aftercommit(),in_txnisfalseand staysfalseuntilbeginTransaction()is called. The no-op only fires on a double-commit, which is benign.Firebird has its own
in_manually_txnflag. Aftercommit(), the flag is cleared and the driver restarts an autocommit transaction internally. The no-op is harmless.MySQL/MariaDB is the only driver where the gap actually exists (between
COMMITand the next SQL statement,SERVER_STATUS_IN_TRANSis cleared). This is the primary use case the attribute is designed for.Backward compatibility
Zero BC break. The attribute is opt-in and defaults to
false. Existing code behaves identically. Only code that explicitly enables the attribute sees the new behavior.Examples
Current workaround (typical Doctrine DBAL
Connectionsubclass)When using Doctrine DBAL with
auto_commit: false, applications must subclassConnectionand overridecommit()to immediately re-open a PDO-level transaction after every commit:This pattern is used by any project combining Doctrine DBAL with
autocommit = 0.With the proposed attribute
Alternatives considered
1. Modify
pdo_mysql_in_transaction()to returntruewhen autocommit is offRejected. This would make
beginTransaction()throw "There is already an active transaction" whenever autocommit is off, breaking existing code. It also changes the semantics ofinTransaction()from "does the server have an active transaction" to "are we logically in a transaction", which is a different question.2. Move the
BEGINworkaround into the MySQL driver's commit handlerRejected. This just moves the redundant
BEGINfrom userland to C. The same SQL is emitted, the same round-trip is wasted. It solves nothing.3. Change the default behavior (no opt-in attribute)
Rejected. Existing code may rely on the exception to detect programming errors (e.g., double-commit). An opt-in attribute avoids any BC break.
Test coverage
6
.phpttests inext/pdo_mysql/tests/, all passing against MySQL 8.4:pdo_mysql_autocommit_aware_commit.phpt—commit()in the gap returnstruepdo_mysql_autocommit_aware_rollback.phpt—rollBack()in the gap returnstruepdo_mysql_autocommit_aware_intransaction.phpt—inTransaction()unchangedpdo_mysql_autocommit_aware_begintransaction.phpt—beginTransaction()unchangedpdo_mysql_autocommit_aware_default_off.phpt— attribute defaults to off (BC preserved)pdo_mysql_autocommit_aware_with_autocommit_on.phpt— no effect when autocommit onNo regressions on existing PDO / PDO MySQL tests.
References
PHP source code
ext/pdo_mysql/mysql_driver.c—pdo_mysql_in_transaction()ext/pdo/pdo_dbh.c—pdo_is_in_transaction(),PDO::commit(),PDO::rollBack()ext/pdo_mysql/tests/pdo_mysql_inTransaction.phptext/pdo_mysql/tests/pdo_mysql_attr_autocommit.phptExisting bug reports and issues
inTransaction()returns false inside transaction (DDL implicit commit)inTransaction()reports false in SQLite transactionPDO::rollback()throws after implicit commitMySQL documentation
SERVER_STATUS_IN_TRANS