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:
4
NEWS
4
NEWS
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
26
ext/pdo_sqlite/pdo_sqlite_arginfo.h
generated
26
ext/pdo_sqlite/pdo_sqlite_arginfo.h
generated
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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*);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user