Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions ext/pdo/pdo_dbh.c
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,10 @@ PHP_METHOD(PDO, commit)
PDO_CONSTRUCT_CHECK;

if (!pdo_is_in_transaction(dbh)) {
/* autocommit off: always logically in a transaction, no-op is safe */
if (dbh->autocommit_aware_txn && !dbh->auto_commit) {
RETURN_TRUE;
}
zend_throw_exception_ex(php_pdo_get_exception(), 0, "There is no active transaction");
RETURN_THROWS();
}
Expand All @@ -750,6 +754,10 @@ PHP_METHOD(PDO, rollBack)
PDO_CONSTRUCT_CHECK;

if (!pdo_is_in_transaction(dbh)) {
/* see commit() */
if (dbh->autocommit_aware_txn && !dbh->auto_commit) {
RETURN_TRUE;
}
zend_throw_exception_ex(php_pdo_get_exception(), 0, "There is no active transaction");
RETURN_THROWS();
}
Expand Down Expand Up @@ -943,6 +951,13 @@ static bool pdo_dbh_attribute_set(pdo_dbh_t *dbh, zend_long attr, zval *value, u
}
return true;
}
case PDO_ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS:
if (!pdo_get_bool_param(&bval, value)) {
return false;
}
dbh->autocommit_aware_txn = bval;
return true;

/* Don't throw a ValueError as the attribute might be a driver specific one */
default:;
}
Expand Down Expand Up @@ -1030,6 +1045,9 @@ PHP_METHOD(PDO, getAttribute)
case PDO_ATTR_STRINGIFY_FETCHES:
RETURN_BOOL(dbh->stringify);

case PDO_ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS:
RETURN_BOOL(dbh->autocommit_aware_txn);

default:
break;
}
Expand Down
2 changes: 2 additions & 0 deletions ext/pdo/pdo_dbh.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ class PDO
public const int ATTR_DEFAULT_FETCH_MODE = UNKNOWN;
/** @cvalue LONG_CONST(PDO_ATTR_DEFAULT_STR_PARAM) */
public const int ATTR_DEFAULT_STR_PARAM = UNKNOWN;
/** @cvalue LONG_CONST(PDO_ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS) */
public const int ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS = UNKNOWN;

/** @cvalue LONG_CONST(PDO_ERRMODE_SILENT) */
public const int ERRMODE_SILENT = UNKNOWN;
Expand Down
8 changes: 7 additions & 1 deletion ext/pdo/pdo_dbh_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions ext/pdo/php_pdo_driver.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ enum pdo_attribute_type {
PDO_ATTR_DEFAULT_FETCH_MODE, /* Set the default fetch mode */
PDO_ATTR_EMULATE_PREPARES, /* use query emulation rather than native */
PDO_ATTR_DEFAULT_STR_PARAM, /* set the default string parameter type (see the PDO::PARAM_STR_* magic flags) */
PDO_ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS, /* suppress exception from commit()/rollBack() when autocommit is off and no transaction is active */

/* this defines the start of the range for driver specific options.
* Drivers should define their own attribute constants beginning with this
Expand Down Expand Up @@ -457,6 +458,10 @@ struct _pdo_dbh_t {
/* if true, commit or rollBack is allowed to be called */
bool in_txn:1;

/* if true, commit()/rollBack() return true instead of throwing when
* autocommit is off and no transaction is active */
bool autocommit_aware_txn:1;

/* when set, convert int/floats to strings */
bool stringify:1;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
--TEST--
PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS - beginTransaction() still works in gap
--DESCRIPTION--
beginTransaction() must remain fully functional. In the gap after COMMIT
(when SERVER_STATUS_IN_TRANS is cleared), beginTransaction() should succeed
and start an explicit transaction.
--EXTENSIONS--
pdo_mysql
--SKIPIF--
<?php
require_once __DIR__ . '/inc/mysql_pdo_test.inc';
MySQLPDOTest::skip();
if (!defined('PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS')) {
die('skip PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS not available');
}
?>
--FILE--
<?php
require_once __DIR__ . '/inc/mysql_pdo_test.inc';

$db = MySQLPDOTest::factory();
$db->setAttribute(PDO::ATTR_AUTOCOMMIT, false);
$db->setAttribute(PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS, true);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$db->exec('DROP TABLE IF EXISTS test_autocommit_aware_bt');
$db->exec('CREATE TABLE test_autocommit_aware_bt (id INT PRIMARY KEY) ENGINE=InnoDB');

// 1. Commit to enter the gap
$db->exec('INSERT INTO test_autocommit_aware_bt VALUES (1)');
$db->commit();
var_dump($db->inTransaction()); // false — in the gap

// 2. beginTransaction() works in the gap
$db->beginTransaction();
var_dump($db->inTransaction()); // true — explicit transaction started

// 3. Operations within explicit transaction
$db->exec('INSERT INTO test_autocommit_aware_bt VALUES (2)');
$db->commit();

// 4. beginTransaction() still throws when already in a transaction
$db->beginTransaction();
try {
$db->beginTransaction();
echo "ERROR: should have thrown\n";
} catch (PDOException $e) {
echo $e->getMessage() . "\n";
}
$db->rollBack();

// 5. Verify data
$stmt = $db->query('SELECT id FROM test_autocommit_aware_bt ORDER BY id');
var_dump($stmt->fetchAll(PDO::FETCH_COLUMN));

$db->exec('DROP TABLE test_autocommit_aware_bt');
?>
--CLEAN--
<?php
require __DIR__ . '/inc/mysql_pdo_test.inc';
$db = MySQLPDOTest::factory();
$db->query('DROP TABLE IF EXISTS test_autocommit_aware_bt');
?>
--EXPECT--
bool(false)
bool(true)
There is already an active transaction
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
65 changes: 65 additions & 0 deletions ext/pdo_mysql/tests/pdo_mysql_autocommit_aware_commit.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
--TEST--
PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS - commit() in gap does not throw
--EXTENSIONS--
pdo_mysql
--SKIPIF--
<?php
require_once __DIR__ . '/inc/mysql_pdo_test.inc';
MySQLPDOTest::skip();
if (!defined('PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS')) {
die('skip PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS not available');
}
?>
--FILE--
<?php
require_once __DIR__ . '/inc/mysql_pdo_test.inc';

$db = MySQLPDOTest::factory();
$db->setAttribute(PDO::ATTR_AUTOCOMMIT, false);
$db->setAttribute(PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS, true);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$db->exec('DROP TABLE IF EXISTS test_autocommit_aware');
$db->exec('CREATE TABLE test_autocommit_aware (id INT PRIMARY KEY) ENGINE=InnoDB');

// 1. Normal commit works
$db->exec('INSERT INTO test_autocommit_aware VALUES (1)');
var_dump($db->inTransaction()); // true — SERVER_STATUS_IN_TRANS set
$result = $db->commit();
var_dump($result); // true
var_dump($db->inTransaction()); // false — flag cleared by COMMIT

// 2. Second commit in the gap: should return true silently (no-op)
$result = $db->commit();
var_dump($result); // true — no exception thrown

// 3. Subsequent operations still work
$db->exec('INSERT INTO test_autocommit_aware VALUES (2)');
var_dump($db->inTransaction()); // true
$db->commit();

// 4. Verify both rows were persisted
$stmt = $db->query('SELECT id FROM test_autocommit_aware ORDER BY id');
$rows = $stmt->fetchAll(PDO::FETCH_COLUMN);
var_dump($rows);

$db->exec('DROP TABLE test_autocommit_aware');
?>
--CLEAN--
<?php
require __DIR__ . '/inc/mysql_pdo_test.inc';
$db = MySQLPDOTest::factory();
$db->query('DROP TABLE IF EXISTS test_autocommit_aware');
?>
--EXPECT--
bool(true)
bool(true)
bool(false)
bool(true)
bool(true)
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
62 changes: 62 additions & 0 deletions ext/pdo_mysql/tests/pdo_mysql_autocommit_aware_default_off.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
--TEST--
PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS - defaults to off (BC preserved)
--DESCRIPTION--
Without explicitly enabling the attribute, the current behavior must be
preserved: commit() and rollBack() throw when there is no active transaction.
--EXTENSIONS--
pdo_mysql
--SKIPIF--
<?php
require_once __DIR__ . '/inc/mysql_pdo_test.inc';
MySQLPDOTest::skip();
if (!defined('PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS')) {
die('skip PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS not available');
}
?>
--FILE--
<?php
require_once __DIR__ . '/inc/mysql_pdo_test.inc';

$db = MySQLPDOTest::factory();
$db->setAttribute(PDO::ATTR_AUTOCOMMIT, false);
// NOT setting ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS — defaults to false
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$db->exec('DROP TABLE IF EXISTS test_autocommit_aware_do');
$db->exec('CREATE TABLE test_autocommit_aware_do (id INT) ENGINE=InnoDB');

// Execute a DML and commit to enter the gap
$db->exec('INSERT INTO test_autocommit_aware_do VALUES (1)');
$db->commit();

// commit() in the gap should throw (current behavior preserved)
try {
$db->commit();
echo "ERROR: should have thrown\n";
} catch (PDOException $e) {
echo "commit: " . $e->getMessage() . "\n";
}

// rollBack() in the gap should throw (current behavior preserved)
try {
$db->rollBack();
echo "ERROR: should have thrown\n";
} catch (PDOException $e) {
echo "rollBack: " . $e->getMessage() . "\n";
}

// Verify attribute defaults to false
var_dump($db->getAttribute(PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS));

$db->exec('DROP TABLE test_autocommit_aware_do');
?>
--CLEAN--
<?php
require __DIR__ . '/inc/mysql_pdo_test.inc';
$db = MySQLPDOTest::factory();
$db->query('DROP TABLE IF EXISTS test_autocommit_aware_do');
?>
--EXPECT--
commit: There is no active transaction
rollBack: There is no active transaction
bool(false)
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
--TEST--
PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS - inTransaction() behavior unchanged
--DESCRIPTION--
The attribute must NOT change inTransaction() semantics. It should still
reflect the actual SERVER_STATUS_IN_TRANS flag from MySQL.
--EXTENSIONS--
pdo_mysql
--SKIPIF--
<?php
require_once __DIR__ . '/inc/mysql_pdo_test.inc';
MySQLPDOTest::skip();
if (!defined('PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS')) {
die('skip PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS not available');
}
?>
--FILE--
<?php
require_once __DIR__ . '/inc/mysql_pdo_test.inc';

$db = MySQLPDOTest::factory();
$db->setAttribute(PDO::ATTR_AUTOCOMMIT, false);
$db->setAttribute(PDO::ATTR_AUTOCOMMIT_AWARE_TRANSACTIONS, true);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$db->exec('DROP TABLE IF EXISTS test_autocommit_aware_it');
$db->exec('CREATE TABLE test_autocommit_aware_it (id INT) ENGINE=InnoDB');

// 1. After DDL (implicit commit clears flag) — flag not set
var_dump($db->inTransaction()); // false

// 2. After a DML statement — flag set by implicit transaction
$db->exec('INSERT INTO test_autocommit_aware_it VALUES (1)');
var_dump($db->inTransaction()); // true

// 3. After COMMIT — flag cleared
$db->commit();
var_dump($db->inTransaction()); // false — unchanged behavior

// 4. After explicit BEGIN — flag set
$db->beginTransaction();
var_dump($db->inTransaction()); // true

// 5. After explicit COMMIT — flag cleared
$db->commit();
var_dump($db->inTransaction()); // false

echo "inTransaction() behavior is unchanged\n";

$db->exec('DROP TABLE test_autocommit_aware_it');
?>
--CLEAN--
<?php
require __DIR__ . '/inc/mysql_pdo_test.inc';
$db = MySQLPDOTest::factory();
$db->query('DROP TABLE IF EXISTS test_autocommit_aware_it');
?>
--EXPECT--
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
inTransaction() behavior is unchanged
Loading
Loading