Skip to content

Commit 7b9519a

Browse files
committed
Fix inconsistency in PDO transaction state
This addresses an issue introduced by #4996 and reported in https://bugs.php.net/bug.php?id=80260. Now that PDO::inTransaction() reports the real transaction state of the connection, there may be a mismatch with PDOs internal transaction state (in_tcx). This is compounded by the fact that MySQL performs implicit commits for DDL queries. This patch fixes the issue by making beginTransaction/commit/rollBack work on the real transaction state provided by the driver as well (or falling back to in_tcx if the driver does not support it). This does mean that writing something like $pdo->beginTransaction(); $pdo->exec('CREATE DATABASE ...'); $pdo->rollBack(); // <- illegal will now result in an error, because the CREATE DATABASE already committed the transaction. I believe this behavior is both correct and desired -- otherwise, there is no indication that the code did not behave correctly and the rollBack() was effectively ignored. However, this is also a BC break. Closes GH-6355.
1 parent 6d3695a commit 7b9519a

File tree

4 files changed

+78
-21
lines changed

4 files changed

+78
-21
lines changed

ext/pdo/pdo_dbh.c

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,14 @@ PHP_METHOD(PDO, prepare)
577577
}
578578
/* }}} */
579579

580+
581+
static zend_bool pdo_is_in_transaction(pdo_dbh_t *dbh) {
582+
if (dbh->methods->in_transaction) {
583+
return dbh->methods->in_transaction(dbh);
584+
}
585+
return dbh->in_txn;
586+
}
587+
580588
/* {{{ Initiates a transaction */
581589
PHP_METHOD(PDO, beginTransaction)
582590
{
@@ -586,7 +594,7 @@ PHP_METHOD(PDO, beginTransaction)
586594

587595
PDO_CONSTRUCT_CHECK;
588596

589-
if (dbh->in_txn) {
597+
if (pdo_is_in_transaction(dbh)) {
590598
zend_throw_exception_ex(php_pdo_get_exception(), 0, "There is already an active transaction");
591599
RETURN_THROWS();
592600
}
@@ -617,7 +625,7 @@ PHP_METHOD(PDO, commit)
617625

618626
PDO_CONSTRUCT_CHECK;
619627

620-
if (!dbh->in_txn) {
628+
if (!pdo_is_in_transaction(dbh)) {
621629
zend_throw_exception_ex(php_pdo_get_exception(), 0, "There is no active transaction");
622630
RETURN_THROWS();
623631
}
@@ -641,7 +649,7 @@ PHP_METHOD(PDO, rollBack)
641649

642650
PDO_CONSTRUCT_CHECK;
643651

644-
if (!dbh->in_txn) {
652+
if (!pdo_is_in_transaction(dbh)) {
645653
zend_throw_exception_ex(php_pdo_get_exception(), 0, "There is no active transaction");
646654
RETURN_THROWS();
647655
}
@@ -665,11 +673,7 @@ PHP_METHOD(PDO, inTransaction)
665673

666674
PDO_CONSTRUCT_CHECK;
667675

668-
if (!dbh->methods->in_transaction) {
669-
RETURN_BOOL(dbh->in_txn);
670-
}
671-
672-
RETURN_BOOL(dbh->methods->in_transaction(dbh));
676+
RETURN_BOOL(pdo_is_in_transaction(dbh));
673677
}
674678
/* }}} */
675679

ext/pdo_mysql/tests/pdo_mysql_commit.phpt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@ if (false == MySQLPDOTest::detect_transactional_mysql_engine($db))
2323
// DDL will issue an implicit commit
2424
$db->exec(sprintf('DROP TABLE IF EXISTS test_commit'));
2525
$db->exec(sprintf('CREATE TABLE test_commit(id INT) ENGINE=%s', MySQLPDOTest::detect_transactional_mysql_engine($db)));
26-
if (true !== ($tmp = $db->commit())) {
27-
printf("[002] No commit allowed? [%s] %s\n",
28-
$db->errorCode(), implode(' ', $db->errorInfo()));
26+
try {
27+
$db->commit();
28+
$failed = false;
29+
} catch (PDOException $e) {
30+
$failed = true;
31+
}
32+
if (!$failed) {
33+
printf("[002] Commit should have failed\n");
2934
}
3035

3136
// pdo_transaction_transitions should check this as well...

ext/pdo_mysql/tests/pdo_mysql_inTransaction.phpt

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ const BEGIN = ['BEGIN', 'START TRANSACTION'];
1414
const END = ['COMMIT', 'ROLLBACK'];
1515

1616
$db = MySQLPDOTest::factory();
17-
// $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // mysql does not support
18-
for ($b = 0; $b < count(BEGIN); $b++) {
19-
for ($e = 0; $e < count(END); $e++) {
17+
// $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // mysql does not support
18+
foreach (BEGIN as $begin) {
19+
foreach (END as $end) {
2020
foreach (['exec', 'query', 'execute'] as $w) {
21-
foreach ([BEGIN[$b], END[$e]] as $command) {
21+
foreach ([$begin, $end] as $command) {
2222
switch ($w) {
2323
case 'exec':
2424
$db->exec($command);
@@ -38,6 +38,37 @@ for ($b = 0; $b < count(BEGIN); $b++) {
3838
}
3939
}
4040
}
41+
echo "\n";
42+
43+
// Mixing PDO transaction API and explicit queries.
44+
foreach (END as $end) {
45+
$db->beginTransaction();
46+
var_dump($db->inTransaction());
47+
$db->exec($end);
48+
var_dump($db->inTransaction());
49+
}
50+
51+
$db->exec('START TRANSACTION');
52+
var_dump($db->inTransaction());
53+
$db->rollBack();
54+
var_dump($db->inTransaction());
55+
$db->exec('START TRANSACTION');
56+
var_dump($db->inTransaction());
57+
$db->commit();
58+
var_dump($db->inTransaction());
59+
echo "\n";
60+
61+
// DDL query causes an implicit commit.
62+
$db->beginTransaction();
63+
var_dump($db->inTransaction());
64+
$db->exec('DROP TABLE IF EXISTS test');
65+
var_dump($db->inTransaction());
66+
67+
// We should be able to start a new transaction after the implicit commit.
68+
$db->beginTransaction();
69+
var_dump($db->inTransaction());
70+
$db->commit();
71+
var_dump($db->inTransaction());
4172

4273
?>
4374
--EXPECT--
@@ -65,3 +96,17 @@ bool(true)
6596
bool(false)
6697
bool(true)
6798
bool(false)
99+
100+
bool(true)
101+
bool(false)
102+
bool(true)
103+
bool(false)
104+
bool(true)
105+
bool(false)
106+
bool(true)
107+
bool(false)
108+
109+
bool(true)
110+
bool(false)
111+
bool(true)
112+
bool(false)

ext/pdo_mysql/tests/pdo_mysql_rollback.phpt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,15 @@ if (false == MySQLPDOTest::detect_transactional_mysql_engine($db))
3737
$db->query('DROP TABLE IF EXISTS test2');
3838
$db->query('CREATE TABLE test2(id INT)');
3939
$num++;
40-
$db->rollBack();
41-
$row = $db->query('SELECT COUNT(*) AS _num FROM test')->fetch(PDO::FETCH_ASSOC);
42-
if ($row['_num'] != $num)
43-
printf("[002] ROLLBACK should have no effect because of the implicit COMMIT
44-
triggered by DROP/CREATE TABLE\n");
45-
40+
try {
41+
$db->rollBack();
42+
$failed = false;
43+
} catch (PDOException $e) {
44+
$failed = true;
45+
}
46+
if (!$failed) {
47+
printf("[003] Rollback should have failed\n");
48+
}
4649

4750
$db->query('DROP TABLE IF EXISTS test2');
4851
$db->query('CREATE TABLE test2(id INT) ENGINE=MyISAM');

0 commit comments

Comments
 (0)