From d31d19e53723c18fdb58bc0a49a01962d9286146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 30 Jul 2025 17:33:54 +0200 Subject: [PATCH 1/6] Add PDO_SQLITE_ATTR_TRANSACTION_MODE --- ext/pdo_sqlite/pdo_sqlite.stub.php | 12 +++++++++ ext/pdo_sqlite/pdo_sqlite_arginfo.h | 26 ++++++++++++++++++- ext/pdo_sqlite/php_pdo_sqlite_int.h | 7 ++++- ext/pdo_sqlite/sqlite_driver.c | 40 ++++++++++++++++++++++++++++- 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/ext/pdo_sqlite/pdo_sqlite.stub.php b/ext/pdo_sqlite/pdo_sqlite.stub.php index 4af2d8c55260b..ffb400ac34f9a 100644 --- a/ext/pdo_sqlite/pdo_sqlite.stub.php +++ b/ext/pdo_sqlite/pdo_sqlite.stub.php @@ -39,6 +39,18 @@ 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; + + /** @cvalue PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED */ + public const int ATTR_TRANSACTION_MODE_DEFERRED = UNKNOWN; + + /** @cvalue PDO_SQLITE_ATTR_TRANSACTION_MODE_IMMEDIATE */ + public const int ATTR_TRANSACTION_MODE_IMMEDIATE = UNKNOWN; + + /** @cvalue PDO_SQLITE_ATTR_TRANSACTION_MODE_EXCLUSIVE */ + public const int ATTR_TRANSACTION_MODE_EXCLUSIVE = UNKNOWN; + #if SQLITE_VERSION_NUMBER >= 3043000 public const int EXPLAIN_MODE_PREPARED = 0; public const int EXPLAIN_MODE_EXPLAIN = 1; diff --git a/ext/pdo_sqlite/pdo_sqlite_arginfo.h b/ext/pdo_sqlite/pdo_sqlite_arginfo.h index e2cd71723706e..6ed5081f2ce74 100644 --- a/ext/pdo_sqlite/pdo_sqlite_arginfo.h +++ b/ext/pdo_sqlite/pdo_sqlite_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: c1d4ef325ecb8c8cb312910e8091ca003dc2603a */ + * Stub hash: d8d8c80e426e8af86474f45fb8fe76ef4b17ffa2 */ 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_ATTR_TRANSACTION_MODE_DEFERRED_value; + ZVAL_LONG(&const_ATTR_TRANSACTION_MODE_DEFERRED_value, PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED); + zend_string *const_ATTR_TRANSACTION_MODE_DEFERRED_name = zend_string_init_interned("ATTR_TRANSACTION_MODE_DEFERRED", sizeof("ATTR_TRANSACTION_MODE_DEFERRED") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_ATTR_TRANSACTION_MODE_DEFERRED_name, &const_ATTR_TRANSACTION_MODE_DEFERRED_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_ATTR_TRANSACTION_MODE_DEFERRED_name); + + zval const_ATTR_TRANSACTION_MODE_IMMEDIATE_value; + ZVAL_LONG(&const_ATTR_TRANSACTION_MODE_IMMEDIATE_value, PDO_SQLITE_ATTR_TRANSACTION_MODE_IMMEDIATE); + zend_string *const_ATTR_TRANSACTION_MODE_IMMEDIATE_name = zend_string_init_interned("ATTR_TRANSACTION_MODE_IMMEDIATE", sizeof("ATTR_TRANSACTION_MODE_IMMEDIATE") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_ATTR_TRANSACTION_MODE_IMMEDIATE_name, &const_ATTR_TRANSACTION_MODE_IMMEDIATE_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_ATTR_TRANSACTION_MODE_IMMEDIATE_name); + + zval const_ATTR_TRANSACTION_MODE_EXCLUSIVE_value; + ZVAL_LONG(&const_ATTR_TRANSACTION_MODE_EXCLUSIVE_value, PDO_SQLITE_ATTR_TRANSACTION_MODE_EXCLUSIVE); + zend_string *const_ATTR_TRANSACTION_MODE_EXCLUSIVE_name = zend_string_init_interned("ATTR_TRANSACTION_MODE_EXCLUSIVE", sizeof("ATTR_TRANSACTION_MODE_EXCLUSIVE") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_ATTR_TRANSACTION_MODE_EXCLUSIVE_name, &const_ATTR_TRANSACTION_MODE_EXCLUSIVE_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_ATTR_TRANSACTION_MODE_EXCLUSIVE_name); #if SQLITE_VERSION_NUMBER >= 3043000 zval const_EXPLAIN_MODE_PREPARED_value; diff --git a/ext/pdo_sqlite/php_pdo_sqlite_int.h b/ext/pdo_sqlite/php_pdo_sqlite_int.h index 69ac003356b87..cfaa7a85b1338 100644 --- a/ext/pdo_sqlite/php_pdo_sqlite_int.h +++ b/ext/pdo_sqlite/php_pdo_sqlite_int.h @@ -51,6 +51,7 @@ typedef struct { struct pdo_sqlite_func *funcs; struct pdo_sqlite_collation *collations; zend_fcall_info_cache authorizer_fcc; + zend_long transaction_mode; } pdo_sqlite_db_handle; typedef struct { @@ -75,7 +76,11 @@ 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, + PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED, + PDO_SQLITE_ATTR_TRANSACTION_MODE_IMMEDIATE, + PDO_SQLITE_ATTR_TRANSACTION_MODE_EXCLUSIVE }; typedef int pdo_sqlite_create_collation_callback(void*, int, const void*, int, const void*); diff --git a/ext/pdo_sqlite/sqlite_driver.c b/ext/pdo_sqlite/sqlite_driver.c index 2c907a34f489b..215515091a30e 100644 --- a/ext/pdo_sqlite/sqlite_driver.c +++ b/ext/pdo_sqlite/sqlite_driver.c @@ -255,7 +255,22 @@ 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) { + char *begin_statement; + switch (H->transaction_mode) { + case 0: + case PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED: + begin_statement = "BEGIN DEFERRED TRANSACTION"; + break; + case PDO_SQLITE_ATTR_TRANSACTION_MODE_IMMEDIATE: + begin_statement = "BEGIN IMMEDIATE TRANSACTION"; + break; + case PDO_SQLITE_ATTR_TRANSACTION_MODE_EXCLUSIVE: + begin_statement = "BEGIN EXCLUSIVE TRANSACTION"; + default: + begin_statement = "BEGIN"; + } + + if (sqlite3_exec(H->db, begin_statement, NULL, NULL, NULL) != SQLITE_OK) { pdo_sqlite_error(dbh); return false; } @@ -286,11 +301,22 @@ 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: + zend_long mode; + if (H->transaction_mode == 0) { + mode = PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED; + } else { + mode = H->transaction_mode; + } + ZVAL_LONG(return_value, mode); + break; default: return 0; @@ -326,6 +352,18 @@ 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; + } + if (lval != PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED && + lval != PDO_SQLITE_ATTR_TRANSACTION_MODE_IMMEDIATE && + lval != PDO_SQLITE_ATTR_TRANSACTION_MODE_EXCLUSIVE) { + return false; + } + H->transaction_mode = lval; + return true; + } return false; } From 4cded0c933dab2bfc8c7ad2b6b86de469e84950d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 30 Jul 2025 18:03:16 +0200 Subject: [PATCH 2/6] Fix 'label followed by a declaration is a C23 extension' --- ext/pdo_sqlite/sqlite_driver.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/pdo_sqlite/sqlite_driver.c b/ext/pdo_sqlite/sqlite_driver.c index 215515091a30e..b5f6575a50f6e 100644 --- a/ext/pdo_sqlite/sqlite_driver.c +++ b/ext/pdo_sqlite/sqlite_driver.c @@ -302,6 +302,7 @@ 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; + zend_long transaction_mode; switch (attr) { case PDO_ATTR_CLIENT_VERSION: @@ -309,13 +310,12 @@ static int pdo_sqlite_get_attribute(pdo_dbh_t *dbh, zend_long attr, zval *return ZVAL_STRING(return_value, (char *)sqlite3_libversion()); break; case PDO_SQLITE_ATTR_TRANSACTION_MODE: - zend_long mode; if (H->transaction_mode == 0) { - mode = PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED; + transaction_mode = PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED; } else { - mode = H->transaction_mode; + transaction_mode = H->transaction_mode; } - ZVAL_LONG(return_value, mode); + ZVAL_LONG(return_value, transaction_mode); break; default: From 36a100204daa90644a563e7e082dcb2bac836c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 30 Jul 2025 18:08:39 +0200 Subject: [PATCH 3/6] Add missing break --- ext/pdo_sqlite/sqlite_driver.c | 1 + 1 file changed, 1 insertion(+) diff --git a/ext/pdo_sqlite/sqlite_driver.c b/ext/pdo_sqlite/sqlite_driver.c index b5f6575a50f6e..2c6749fc7f470 100644 --- a/ext/pdo_sqlite/sqlite_driver.c +++ b/ext/pdo_sqlite/sqlite_driver.c @@ -266,6 +266,7 @@ static bool sqlite_handle_begin(pdo_dbh_t *dbh) break; case PDO_SQLITE_ATTR_TRANSACTION_MODE_EXCLUSIVE: begin_statement = "BEGIN EXCLUSIVE TRANSACTION"; + break; default: begin_statement = "BEGIN"; } From dee9fa879d65e02b29eec758d034edc71a7a671f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 30 Jul 2025 20:41:02 +0200 Subject: [PATCH 4/6] Add basic tests for the PHP API --- .../tests/pdo_sqlite_transaction_mode.phpt | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 ext/pdo_sqlite/tests/pdo_sqlite_transaction_mode.phpt diff --git a/ext/pdo_sqlite/tests/pdo_sqlite_transaction_mode.phpt b/ext/pdo_sqlite/tests/pdo_sqlite_transaction_mode.phpt new file mode 100644 index 0000000000000..1be7a7fe75de8 --- /dev/null +++ b/ext/pdo_sqlite/tests/pdo_sqlite_transaction_mode.phpt @@ -0,0 +1,48 @@ +--TEST-- +PDO_sqlite: Testing ATTR_TRANSACTION_MODE +--EXTENSIONS-- +pdo_sqlite +--FILE-- +getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::ATTR_TRANSACTION_MODE_DEFERRED); + +// Both should return true +var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::ATTR_TRANSACTION_MODE_DEFERRED)); +var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::ATTR_TRANSACTION_MODE_DEFERRED); + +// Both should return true +var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::ATTR_TRANSACTION_MODE_IMMEDIATE)); +var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::ATTR_TRANSACTION_MODE_IMMEDIATE); + +// Both should return true +var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::ATTR_TRANSACTION_MODE_EXCLUSIVE)); +var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::ATTR_TRANSACTION_MODE_EXCLUSIVE); + +// Cannot set invalid values +var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 0)); +var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 1)); +var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 123)); + +// Cannot use these as keys, only as values. These should return false +var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE_DEFERRED, true)); +var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE_IMMEDIATE, true)); +var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE_EXCLUSIVE, true)); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) From f5d70c591c0af42fd70ec9efb2f3e84c6e90236a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 30 Jul 2025 21:15:14 +0200 Subject: [PATCH 5/6] Add tests verifying tx type using a lock contention scenario --- .../pdo_sqlite_transaction_mode.phpt | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) rename ext/pdo_sqlite/tests/{ => subclasses}/pdo_sqlite_transaction_mode.phpt (55%) diff --git a/ext/pdo_sqlite/tests/pdo_sqlite_transaction_mode.phpt b/ext/pdo_sqlite/tests/subclasses/pdo_sqlite_transaction_mode.phpt similarity index 55% rename from ext/pdo_sqlite/tests/pdo_sqlite_transaction_mode.phpt rename to ext/pdo_sqlite/tests/subclasses/pdo_sqlite_transaction_mode.phpt index 1be7a7fe75de8..0e4da2ced8338 100644 --- a/ext/pdo_sqlite/tests/pdo_sqlite_transaction_mode.phpt +++ b/ext/pdo_sqlite/tests/subclasses/pdo_sqlite_transaction_mode.phpt @@ -5,7 +5,9 @@ pdo_sqlite --FILE-- getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::ATTR_TRANSACTION_MODE_DEFERRED); @@ -31,6 +33,39 @@ var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 123)); var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE_DEFERRED, true)); var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE_IMMEDIATE, true)); var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE_EXCLUSIVE, true)); + +// 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::ATTR_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\n"); +} +$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::ATTR_TRANSACTION_MODE_IMMEDIATE); +$pdo->beginTransaction(); +try { + $pdo2->exec('begin immediate transaction'); + printf("Database is not locked\n"); +} catch (PDOException $e) { + printf("Database is locked\n"); +} +$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::ATTR_TRANSACTION_MODE_EXCLUSIVE); +$pdo->beginTransaction(); +try { + $pdo2->exec('begin immediate transaction'); + printf("Database is not locked\n"); +} catch (PDOException $e) { + printf("Database is locked\n"); +} ?> --EXPECT-- bool(true) @@ -46,3 +81,6 @@ bool(false) bool(false) bool(false) bool(false) +Database is not locked +Database is locked +Database is locked From 45e82002ba7bd52f1731b5dfb4f5595f32213789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 30 Jul 2025 21:22:58 +0200 Subject: [PATCH 6/6] Assert concrete exception messages --- .../tests/subclasses/pdo_sqlite_transaction_mode.phpt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ext/pdo_sqlite/tests/subclasses/pdo_sqlite_transaction_mode.phpt b/ext/pdo_sqlite/tests/subclasses/pdo_sqlite_transaction_mode.phpt index 0e4da2ced8338..c17838a67cecc 100644 --- a/ext/pdo_sqlite/tests/subclasses/pdo_sqlite_transaction_mode.phpt +++ b/ext/pdo_sqlite/tests/subclasses/pdo_sqlite_transaction_mode.phpt @@ -42,7 +42,7 @@ try { $pdo2->rollBack(); printf("Database is not locked\n"); } catch (PDOException $e) { - printf("Database is locked\n"); + printf("Database is locked: %s\n", $e->getMessage()); } $pdo->rollBack(); @@ -53,7 +53,7 @@ try { $pdo2->exec('begin immediate transaction'); printf("Database is not locked\n"); } catch (PDOException $e) { - printf("Database is locked\n"); + printf("Database is locked: %s\n", $e->getMessage()); } $pdo->rollBack(); @@ -64,7 +64,7 @@ try { $pdo2->exec('begin immediate transaction'); printf("Database is not locked\n"); } catch (PDOException $e) { - printf("Database is locked\n"); + printf("Database is locked: %s\n", $e->getMessage()); } ?> --EXPECT-- @@ -82,5 +82,5 @@ bool(false) bool(false) bool(false) Database is not locked -Database is locked -Database is 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