1
0
mirror of https://github.com/php/php-src.git synced 2026-03-24 00:02:20 +01:00

Implement GH-8967: Add PDO_SQLITE_ATTR_TRANSACTION_MODE (#19317)

This commit implements GH-8967.

SQLite supports multiple transaction modes. These include:
- DEFERRED (default) only acquires a lock when you start a read/write
- IMMEDIATE acquires a reserved lock
- EXCLUSIVE acquires an exclusive lock (stricter than immediate)

In WAL mode IMMEDIATE and EXCLUSIVE are identical.

One reason for wanting to specify a transaction mode is that SQLite
doesn't respect busy_timeout when a DEFERRED transaction tries to
upgrade a read lock to a write lock. Normally if you try to acquire a
lock and have busy_timeout configured, SQLite will wait for that period
until giving up and erroring out (SQLITE_BUSY). With DEFERRED, if you
have a transaction that first reads and there's a concurrent writer
while it's trying to upgrade to a write lock, you will immediately get
SQLITE_BUSY regardless of your busy_timeout.

Prior to this commit, the only available workarounds were:
- Using $pdo->exec("BEGIN IMMEDIATE TRANSACTION") instead of
  $pdo->beginTransaction()
- Doing a dummy write at the start of each transaction so you don't get
  stuck with a read lock

Both of those aren't very usable, especially in a framework context
where the user doesn't have complete control over how transactions are
started.

To address that, this commit adds four class constants to Pdo\Sqlite:
- ATTR_TRANSACTION_MODE -- a new attribute
- TRANSACTION_MODE_DEFERRED = 0
- TRANSACTION_MODE_IMMEDIATE = 1
- TRANSACTION_MODE_EXCLUSIVE = 2

These can be used as:
  $pdo->setAttribute(
    $pdo::ATTR_TRANSACTION_MODE,
    $pdo::TRANSACTION_MODE_IMMEDIATE
  );
This commit is contained in:
Samuel Štancl
2025-09-07 15:42:30 +02:00
committed by GitHub
parent c267652d69
commit a09d919ce8
8 changed files with 170 additions and 3 deletions

4
NEWS
View File

@@ -41,6 +41,10 @@ PHP NEWS
- PDO:
. Driver specific methods in the PDO class are now deprecated. (Arnaud)
- PDO_SQLITE:
. Add PDO\Sqlite::ATTR_TRANSACTION_MODE connection attribute.
(Samuel Štancl)
- Reflection:
. Fix GH-19691 (getModifierNames() not reporting asymmetric visibility).
(DanielEScherzer)

View File

@@ -266,6 +266,11 @@ PHP 8.5 UPGRADE NOTES
. Added class constants Pdo_Sqlite::ATTR_EXPLAIN_STATEMENT,
Pdo_Sqlite::EXPLAIN_MODE_PREPARED, Pdo_Sqlite::EXPLAIN_MODE_EXPLAIN,
Pdo_Sqlite::EXPLAIN_MODE_EXPLAIN_QUERY_PLAN.
. Added PDO\Sqlite::ATTR_TRANSACTION_MODE connection attribute with
possible values PDO\Sqlite::TRANSACTION_MODE_DEFERRED,
PDO\Sqlite::TRANSACTION_MODE_IMMEDIATE,
and PDO\Sqlite::TRANSACTION_MODE_EXCLUSIVE, allowing to configure
the transaction mode to use when calling beginTransaction().
- Session:
. session_set_cookie_params(), session_get_cookie_params(), and session_start()

View File

@@ -39,6 +39,13 @@ class Sqlite extends \PDO
/** @cvalue PDO_SQLITE_ATTR_EXPLAIN_STATEMENT */
public const int ATTR_EXPLAIN_STATEMENT = UNKNOWN;
/** @cvalue PDO_SQLITE_ATTR_TRANSACTION_MODE */
public const int ATTR_TRANSACTION_MODE = UNKNOWN;
public const int TRANSACTION_MODE_DEFERRED = 0;
public const int TRANSACTION_MODE_IMMEDIATE = 1;
public const int TRANSACTION_MODE_EXCLUSIVE = 2;
#if SQLITE_VERSION_NUMBER >= 3043000
public const int EXPLAIN_MODE_PREPARED = 0;
public const int EXPLAIN_MODE_EXPLAIN = 1;

View File

@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: c1d4ef325ecb8c8cb312910e8091ca003dc2603a */
* Stub hash: 721c46905fa8fb1e18d7196ed85c37f56049ea33 */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Pdo_Sqlite_createAggregate, 0, 3, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0)
@@ -121,6 +121,30 @@ static zend_class_entry *register_class_Pdo_Sqlite(zend_class_entry *class_entry
zend_string *const_ATTR_EXPLAIN_STATEMENT_name = zend_string_init_interned("ATTR_EXPLAIN_STATEMENT", sizeof("ATTR_EXPLAIN_STATEMENT") - 1, 1);
zend_declare_typed_class_constant(class_entry, const_ATTR_EXPLAIN_STATEMENT_name, &const_ATTR_EXPLAIN_STATEMENT_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release(const_ATTR_EXPLAIN_STATEMENT_name);
zval const_ATTR_TRANSACTION_MODE_value;
ZVAL_LONG(&const_ATTR_TRANSACTION_MODE_value, PDO_SQLITE_ATTR_TRANSACTION_MODE);
zend_string *const_ATTR_TRANSACTION_MODE_name = zend_string_init_interned("ATTR_TRANSACTION_MODE", sizeof("ATTR_TRANSACTION_MODE") - 1, 1);
zend_declare_typed_class_constant(class_entry, const_ATTR_TRANSACTION_MODE_name, &const_ATTR_TRANSACTION_MODE_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release(const_ATTR_TRANSACTION_MODE_name);
zval const_TRANSACTION_MODE_DEFERRED_value;
ZVAL_LONG(&const_TRANSACTION_MODE_DEFERRED_value, 0);
zend_string *const_TRANSACTION_MODE_DEFERRED_name = zend_string_init_interned("TRANSACTION_MODE_DEFERRED", sizeof("TRANSACTION_MODE_DEFERRED") - 1, 1);
zend_declare_typed_class_constant(class_entry, const_TRANSACTION_MODE_DEFERRED_name, &const_TRANSACTION_MODE_DEFERRED_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release(const_TRANSACTION_MODE_DEFERRED_name);
zval const_TRANSACTION_MODE_IMMEDIATE_value;
ZVAL_LONG(&const_TRANSACTION_MODE_IMMEDIATE_value, 1);
zend_string *const_TRANSACTION_MODE_IMMEDIATE_name = zend_string_init_interned("TRANSACTION_MODE_IMMEDIATE", sizeof("TRANSACTION_MODE_IMMEDIATE") - 1, 1);
zend_declare_typed_class_constant(class_entry, const_TRANSACTION_MODE_IMMEDIATE_name, &const_TRANSACTION_MODE_IMMEDIATE_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release(const_TRANSACTION_MODE_IMMEDIATE_name);
zval const_TRANSACTION_MODE_EXCLUSIVE_value;
ZVAL_LONG(&const_TRANSACTION_MODE_EXCLUSIVE_value, 2);
zend_string *const_TRANSACTION_MODE_EXCLUSIVE_name = zend_string_init_interned("TRANSACTION_MODE_EXCLUSIVE", sizeof("TRANSACTION_MODE_EXCLUSIVE") - 1, 1);
zend_declare_typed_class_constant(class_entry, const_TRANSACTION_MODE_EXCLUSIVE_name, &const_TRANSACTION_MODE_EXCLUSIVE_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release(const_TRANSACTION_MODE_EXCLUSIVE_name);
#if SQLITE_VERSION_NUMBER >= 3043000
zval const_EXPLAIN_MODE_PREPARED_value;

View File

@@ -27,6 +27,12 @@ extern zend_module_entry pdo_sqlite_module_entry;
#include "TSRM.h"
#endif
enum pdo_sqlite_transaction_mode {
PDO_SQLITE_TRANSACTION_MODE_DEFERRED = 0,
PDO_SQLITE_TRANSACTION_MODE_IMMEDIATE = 1,
PDO_SQLITE_TRANSACTION_MODE_EXCLUSIVE = 2
};
PHP_MINIT_FUNCTION(pdo_sqlite);
PHP_MSHUTDOWN_FUNCTION(pdo_sqlite);
PHP_RINIT_FUNCTION(pdo_sqlite);

View File

@@ -51,6 +51,7 @@ typedef struct {
struct pdo_sqlite_func *funcs;
struct pdo_sqlite_collation *collations;
zend_fcall_info_cache authorizer_fcc;
enum pdo_sqlite_transaction_mode transaction_mode;
} pdo_sqlite_db_handle;
typedef struct {
@@ -75,7 +76,8 @@ enum {
PDO_SQLITE_ATTR_READONLY_STATEMENT,
PDO_SQLITE_ATTR_EXTENDED_RESULT_CODES,
PDO_SQLITE_ATTR_BUSY_STATEMENT,
PDO_SQLITE_ATTR_EXPLAIN_STATEMENT
PDO_SQLITE_ATTR_EXPLAIN_STATEMENT,
PDO_SQLITE_ATTR_TRANSACTION_MODE
};
typedef int pdo_sqlite_create_collation_callback(void*, int, const void*, int, const void*);

View File

@@ -255,7 +255,20 @@ static bool sqlite_handle_begin(pdo_dbh_t *dbh)
{
pdo_sqlite_db_handle *H = (pdo_sqlite_db_handle *)dbh->driver_data;
if (sqlite3_exec(H->db, "BEGIN", NULL, NULL, NULL) != SQLITE_OK) {
const char *begin_statement = "BEGIN";
switch (H->transaction_mode) {
case PDO_SQLITE_TRANSACTION_MODE_DEFERRED:
begin_statement = "BEGIN DEFERRED TRANSACTION";
break;
case PDO_SQLITE_TRANSACTION_MODE_IMMEDIATE:
begin_statement = "BEGIN IMMEDIATE TRANSACTION";
break;
case PDO_SQLITE_TRANSACTION_MODE_EXCLUSIVE:
begin_statement = "BEGIN EXCLUSIVE TRANSACTION";
break;
}
if (sqlite3_exec(H->db, begin_statement, NULL, NULL, NULL) != SQLITE_OK) {
pdo_sqlite_error(dbh);
return false;
}
@@ -286,11 +299,16 @@ static bool sqlite_handle_rollback(pdo_dbh_t *dbh)
static int pdo_sqlite_get_attribute(pdo_dbh_t *dbh, zend_long attr, zval *return_value)
{
pdo_sqlite_db_handle *H = (pdo_sqlite_db_handle *)dbh->driver_data;
switch (attr) {
case PDO_ATTR_CLIENT_VERSION:
case PDO_ATTR_SERVER_VERSION:
ZVAL_STRING(return_value, (char *)sqlite3_libversion());
break;
case PDO_SQLITE_ATTR_TRANSACTION_MODE:
ZVAL_LONG(return_value, H->transaction_mode);
break;
default:
return 0;
@@ -326,6 +344,19 @@ static bool pdo_sqlite_set_attr(pdo_dbh_t *dbh, zend_long attr, zval *val)
}
sqlite3_extended_result_codes(H->db, lval);
return true;
case PDO_SQLITE_ATTR_TRANSACTION_MODE:
if (!pdo_get_long_param(&lval, val)) {
return false;
}
switch (lval) {
case PDO_SQLITE_TRANSACTION_MODE_DEFERRED:
case PDO_SQLITE_TRANSACTION_MODE_IMMEDIATE:
case PDO_SQLITE_TRANSACTION_MODE_EXCLUSIVE:
H->transaction_mode = lval;
return true;
default:
return false;
}
}
return false;
}

View File

@@ -0,0 +1,88 @@
--TEST--
PDO_sqlite: Testing ATTR_TRANSACTION_MODE
--EXTENSIONS--
pdo_sqlite
--FILE--
<?php
$dsn = 'sqlite:file:foo?mode=memory&cache=shared';
$pdo = PDO::connect($dsn);
$pdo2 = PDO::connect($dsn);
// Deferred by default before any transaction mode is set
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::TRANSACTION_MODE_DEFERRED);
// Both should return true - setting DEFERRED
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::TRANSACTION_MODE_DEFERRED));
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::TRANSACTION_MODE_DEFERRED);
// Both should return true - setting IMMEDIATE
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::TRANSACTION_MODE_IMMEDIATE));
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::TRANSACTION_MODE_IMMEDIATE);
// Both should return true - setting EXCLUSIVE
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::TRANSACTION_MODE_EXCLUSIVE));
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::TRANSACTION_MODE_EXCLUSIVE);
// Setting the numeric equivalents of the above. All should return true
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 0));
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::TRANSACTION_MODE_DEFERRED);
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 1));
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::TRANSACTION_MODE_IMMEDIATE);
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 2));
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::TRANSACTION_MODE_EXCLUSIVE);
// Cannot set a random numeric value
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 4));
// Set $pdo to deferred, try to get immediate transaction in $pdo2. There should be no lock contention
$pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::TRANSACTION_MODE_DEFERRED);
$pdo->beginTransaction();
try {
$pdo2->exec('begin immediate transaction');
$pdo2->rollBack();
printf("Database is not locked\n");
} catch (PDOException $e) {
printf("Database is locked: %s\n", $e->getMessage());
}
$pdo->rollBack();
// Set $pdo to immediate, try to get immediate transaction in $pdo2. There SHOULD be lock contention
$pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::TRANSACTION_MODE_IMMEDIATE);
$pdo->beginTransaction();
try {
$pdo2->exec('begin immediate transaction');
printf("Database is not locked\n");
} catch (PDOException $e) {
printf("Database is locked: %s\n", $e->getMessage());
}
$pdo->rollBack();
// Set $pdo to exclusive, try to get immediate transaction in $pdo2. There SHOULD be lock contention
$pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::TRANSACTION_MODE_EXCLUSIVE);
$pdo->beginTransaction();
try {
$pdo2->exec('begin immediate transaction');
printf("Database is not locked\n");
} catch (PDOException $e) {
printf("Database is locked: %s\n", $e->getMessage());
}
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(false)
Database is not locked
Database is locked: SQLSTATE[HY000]: General error: 6 database table is locked
Database is locked: SQLSTATE[HY000]: General error: 6 database schema is locked: main