]> granicus.if.org Git - php/commitdiff
Fix inconsistency in PDO transaction state
authorNikita Popov <nikita.ppv@gmail.com>
Tue, 20 Oct 2020 09:29:47 +0000 (11:29 +0200)
committerNikita Popov <nikita.ppv@gmail.com>
Mon, 26 Oct 2020 16:01:18 +0000 (17:01 +0100)
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.

ext/pdo/pdo_dbh.c
ext/pdo_mysql/tests/pdo_mysql_commit.phpt
ext/pdo_mysql/tests/pdo_mysql_inTransaction.phpt
ext/pdo_mysql/tests/pdo_mysql_rollback.phpt

index a0c2b74c331f52de5a4d727a077ffa9bb5cbef19..eb77693f5bf6b8dee25f8a68fa598f9c42b39dfc 100644 (file)
@@ -577,6 +577,14 @@ PHP_METHOD(PDO, prepare)
 }
 /* }}} */
 
+
+static zend_bool pdo_is_in_transaction(pdo_dbh_t *dbh) {
+       if (dbh->methods->in_transaction) {
+               return dbh->methods->in_transaction(dbh);
+       }
+       return dbh->in_txn;
+}
+
 /* {{{ Initiates a transaction */
 PHP_METHOD(PDO, beginTransaction)
 {
@@ -586,7 +594,7 @@ PHP_METHOD(PDO, beginTransaction)
 
        PDO_CONSTRUCT_CHECK;
 
-       if (dbh->in_txn) {
+       if (pdo_is_in_transaction(dbh)) {
                zend_throw_exception_ex(php_pdo_get_exception(), 0, "There is already an active transaction");
                RETURN_THROWS();
        }
@@ -617,7 +625,7 @@ PHP_METHOD(PDO, commit)
 
        PDO_CONSTRUCT_CHECK;
 
-       if (!dbh->in_txn) {
+       if (!pdo_is_in_transaction(dbh)) {
                zend_throw_exception_ex(php_pdo_get_exception(), 0, "There is no active transaction");
                RETURN_THROWS();
        }
@@ -641,7 +649,7 @@ PHP_METHOD(PDO, rollBack)
 
        PDO_CONSTRUCT_CHECK;
 
-       if (!dbh->in_txn) {
+       if (!pdo_is_in_transaction(dbh)) {
                zend_throw_exception_ex(php_pdo_get_exception(), 0, "There is no active transaction");
                RETURN_THROWS();
        }
@@ -665,11 +673,7 @@ PHP_METHOD(PDO, inTransaction)
 
        PDO_CONSTRUCT_CHECK;
 
-       if (!dbh->methods->in_transaction) {
-               RETURN_BOOL(dbh->in_txn);
-       }
-
-       RETURN_BOOL(dbh->methods->in_transaction(dbh));
+       RETURN_BOOL(pdo_is_in_transaction(dbh));
 }
 /* }}} */
 
index 506be6475f35c5c742c48a12393c61edf31892f0..f88ae149faf16b6893726a13932824ec2bea326f 100644 (file)
@@ -23,9 +23,14 @@ if (false == MySQLPDOTest::detect_transactional_mysql_engine($db))
         // DDL will issue an implicit commit
         $db->exec(sprintf('DROP TABLE IF EXISTS test_commit'));
         $db->exec(sprintf('CREATE TABLE test_commit(id INT) ENGINE=%s', MySQLPDOTest::detect_transactional_mysql_engine($db)));
-        if (true !== ($tmp = $db->commit())) {
-            printf("[002] No commit allowed? [%s] %s\n",
-                $db->errorCode(), implode(' ', $db->errorInfo()));
+        try {
+            $db->commit();
+            $failed = false;
+        } catch (PDOException $e) {
+            $failed = true;
+        }
+        if (!$failed) {
+            printf("[002] Commit should have failed\n");
         }
 
         // pdo_transaction_transitions should check this as well...
index 6d82e22b1238f2d34aba3b0a2ab4c5d51b61094c..7ce93e8409a36f332acaddaa4750e62212bf33dd 100644 (file)
@@ -14,11 +14,11 @@ const BEGIN = ['BEGIN', 'START TRANSACTION'];
 const END = ['COMMIT', 'ROLLBACK'];
 
 $db = MySQLPDOTest::factory();
-// $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // mysql does not support
-for ($b = 0; $b < count(BEGIN); $b++) {
-    for ($e = 0; $e < count(END); $e++) {
+// $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // mysql does not support
+foreach (BEGIN as $begin) {
+    foreach (END as $end) {
         foreach (['exec', 'query', 'execute'] as $w) {
-            foreach ([BEGIN[$b], END[$e]] as $command) {
+            foreach ([$begin, $end] as $command) {
                 switch ($w) {
                     case 'exec':
                         $db->exec($command);
@@ -38,6 +38,37 @@ for ($b = 0; $b < count(BEGIN); $b++) {
         }
     }
 }
+echo "\n";
+
+// Mixing PDO transaction API and explicit queries.
+foreach (END as $end) {
+    $db->beginTransaction();
+    var_dump($db->inTransaction());
+    $db->exec($end);
+    var_dump($db->inTransaction());
+}
+
+$db->exec('START TRANSACTION');
+var_dump($db->inTransaction());
+$db->rollBack();
+var_dump($db->inTransaction());
+$db->exec('START TRANSACTION');
+var_dump($db->inTransaction());
+$db->commit();
+var_dump($db->inTransaction());
+echo "\n";
+
+// DDL query causes an implicit commit.
+$db->beginTransaction();
+var_dump($db->inTransaction());
+$db->exec('DROP TABLE IF EXISTS test');
+var_dump($db->inTransaction());
+
+// We should be able to start a new transaction after the implicit commit.
+$db->beginTransaction();
+var_dump($db->inTransaction());
+$db->commit();
+var_dump($db->inTransaction());
 
 ?>
 --EXPECT--
@@ -65,3 +96,17 @@ bool(true)
 bool(false)
 bool(true)
 bool(false)
+
+bool(true)
+bool(false)
+bool(true)
+bool(false)
+bool(true)
+bool(false)
+bool(true)
+bool(false)
+
+bool(true)
+bool(false)
+bool(true)
+bool(false)
index d27aefc6be46a291b9f83d501a7e3489654b3014..25a467d877fdefad9be834de364d649e59b24963 100644 (file)
@@ -37,12 +37,15 @@ if (false == MySQLPDOTest::detect_transactional_mysql_engine($db))
     $db->query('DROP TABLE IF EXISTS test2');
     $db->query('CREATE TABLE test2(id INT)');
     $num++;
-    $db->rollBack();
-    $row = $db->query('SELECT COUNT(*) AS _num FROM test')->fetch(PDO::FETCH_ASSOC);
-    if ($row['_num'] != $num)
-        printf("[002] ROLLBACK should have no effect because of the implicit COMMIT
-            triggered by DROP/CREATE TABLE\n");
-
+    try {
+        $db->rollBack();
+        $failed = false;
+    } catch (PDOException $e) {
+        $failed = true;
+    }
+    if (!$failed) {
+        printf("[003] Rollback should have failed\n");
+    }
 
     $db->query('DROP TABLE IF EXISTS test2');
     $db->query('CREATE TABLE test2(id INT) ENGINE=MyISAM');