From 1b63b7d4bdaf1db29291179947589ed728d3c658 Mon Sep 17 00:00:00 2001 From: Juan Versolato Lopes Date: Tue, 3 Nov 2026 12:25:26 -0300 Subject: [PATCH 1/3] Implement GH-13597: Add Pdo\Sqlite::ATTR_DQS SQLite accepts double-quoted string literals by default when a name in double quotes does not resolve to a column, so SELECT "abc" silently returns the string 'abc'. SQLite documents this as a misfeature, and the sqlite3 shell turns it off since 3.41.0. Through PDO there was no way to turn it off, so queries that only work because of this fallback pass in PHP and then fail elsewhere. Add a boolean connection attribute that sets SQLITE_DBCONFIG_DQS_DML and SQLITE_DBCONFIG_DQS_DDL together. It can be passed to the constructor or set later, and getAttribute() reports whether either is still enabled. The default is unchanged. The constant only exists when building against SQLite 3.29.0 or later, which is where these options were introduced. --- NEWS | 2 + UPGRADING | 6 +++ ext/pdo_sqlite/pdo_sqlite.stub.php | 5 +++ ext/pdo_sqlite/pdo_sqlite_arginfo.h | 10 ++++- ext/pdo_sqlite/php_pdo_sqlite_int.h | 3 +- ext/pdo_sqlite/sqlite_driver.c | 25 +++++++++++ ext/pdo_sqlite/tests/gh13597.phpt | 67 +++++++++++++++++++++++++++++ 7 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 ext/pdo_sqlite/tests/gh13597.phpt diff --git a/NEWS b/NEWS index 86cb13963862..4c9c5645a00f 100644 --- a/NEWS +++ b/NEWS @@ -102,6 +102,8 @@ PHP NEWS - PDO_SQLite: . Fixed bug GH-24990 (Statement unusable after a failed execute()). (lazerg) + . Added Pdo\Sqlite::ATTR_DQS to disable double-quoted string literals + (GH-13597). (Juan Versolato Lopes) - PGSQL: . Fixed pg_lo_write() rejecting data containing null bytes. (Ilia Alshanetsky) diff --git a/UPGRADING b/UPGRADING index 9804b8706b81..dc41efa430c4 100644 --- a/UPGRADING +++ b/UPGRADING @@ -84,6 +84,12 @@ PHP 8.7 UPGRADE NOTES A ValueError is thrown if the array is empty or contains an invalid locale, and a TypeError if it contains a non-string value. +- PDO_SQLite: + . Added the Pdo\Sqlite::ATTR_DQS connection attribute. Setting it to false + makes SQLite reject double-quoted string literals, so "abc" is only read + as an identifier, as in the sqlite3 shell since SQLite 3.41.0. It is + available when PHP is built against SQLite 3.29.0 or later. + - Streams: . The so_linger, so_rcvbuf and so_sndbuf stream socket context options are now also applied to unix and unix datagram sockets. diff --git a/ext/pdo_sqlite/pdo_sqlite.stub.php b/ext/pdo_sqlite/pdo_sqlite.stub.php index 53f1ceba427b..5a7336639195 100644 --- a/ext/pdo_sqlite/pdo_sqlite.stub.php +++ b/ext/pdo_sqlite/pdo_sqlite.stub.php @@ -42,6 +42,11 @@ class Sqlite extends \PDO /** @cvalue PDO_SQLITE_ATTR_TRANSACTION_MODE */ public const int ATTR_TRANSACTION_MODE = UNKNOWN; +#ifdef SQLITE_DBCONFIG_DQS_DML + /** @cvalue PDO_SQLITE_ATTR_DQS */ + public const int ATTR_DQS = UNKNOWN; +#endif + public const int TRANSACTION_MODE_DEFERRED = 0; public const int TRANSACTION_MODE_IMMEDIATE = 1; public const int TRANSACTION_MODE_EXCLUSIVE = 2; diff --git a/ext/pdo_sqlite/pdo_sqlite_arginfo.h b/ext/pdo_sqlite/pdo_sqlite_arginfo.h index 73a9158301c0..303b4b49fcc0 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 pdo_sqlite.stub.php instead. - * Stub hash: 721c46905fa8fb1e18d7196ed85c37f56049ea33 */ + * Stub hash: ffc048d27553b31bbbd6cf33751c983339058285 */ 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) @@ -127,6 +127,14 @@ static zend_class_entry *register_class_Pdo_Sqlite(zend_class_entry *class_entry zend_string *const_ATTR_TRANSACTION_MODE_name = zend_string_init_interned("ATTR_TRANSACTION_MODE", sizeof("ATTR_TRANSACTION_MODE") - 1, true); 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_ex(const_ATTR_TRANSACTION_MODE_name, true); +#if defined(SQLITE_DBCONFIG_DQS_DML) + + zval const_ATTR_DQS_value; + ZVAL_LONG(&const_ATTR_DQS_value, PDO_SQLITE_ATTR_DQS); + zend_string *const_ATTR_DQS_name = zend_string_init_interned("ATTR_DQS", sizeof("ATTR_DQS") - 1, true); + zend_declare_typed_class_constant(class_entry, const_ATTR_DQS_name, &const_ATTR_DQS_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release_ex(const_ATTR_DQS_name, true); +#endif zval const_TRANSACTION_MODE_DEFERRED_value; ZVAL_LONG(&const_TRANSACTION_MODE_DEFERRED_value, 0); diff --git a/ext/pdo_sqlite/php_pdo_sqlite_int.h b/ext/pdo_sqlite/php_pdo_sqlite_int.h index 122f714040de..5535f86f01f8 100644 --- a/ext/pdo_sqlite/php_pdo_sqlite_int.h +++ b/ext/pdo_sqlite/php_pdo_sqlite_int.h @@ -75,7 +75,8 @@ enum { PDO_SQLITE_ATTR_EXTENDED_RESULT_CODES, PDO_SQLITE_ATTR_BUSY_STATEMENT, PDO_SQLITE_ATTR_EXPLAIN_STATEMENT, - PDO_SQLITE_ATTR_TRANSACTION_MODE + PDO_SQLITE_ATTR_TRANSACTION_MODE, + PDO_SQLITE_ATTR_DQS }; 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 692bb8fd6c86..6de50ea76442 100644 --- a/ext/pdo_sqlite/sqlite_driver.c +++ b/ext/pdo_sqlite/sqlite_driver.c @@ -303,6 +303,15 @@ static int pdo_sqlite_get_attribute(pdo_dbh_t *dbh, zend_long attr, zval *return case PDO_SQLITE_ATTR_TRANSACTION_MODE: ZVAL_LONG(return_value, H->transaction_mode); break; +#ifdef SQLITE_DBCONFIG_DQS_DML + case PDO_SQLITE_ATTR_DQS: { + int dml = 0, ddl = 0; + sqlite3_db_config(H->db, SQLITE_DBCONFIG_DQS_DML, -1, &dml); + sqlite3_db_config(H->db, SQLITE_DBCONFIG_DQS_DDL, -1, &ddl); + ZVAL_BOOL(return_value, dml || ddl); + break; + } +#endif default: return 0; @@ -351,6 +360,22 @@ static bool pdo_sqlite_set_attr(pdo_dbh_t *dbh, zend_long attr, zval *val) default: return false; } +#ifdef SQLITE_DBCONFIG_DQS_DML + case PDO_SQLITE_ATTR_DQS: { + bool bval; + if (!pdo_get_bool_param(&bval, val)) { + return false; + } + /* Double-quoted string literals are controlled separately for DML + * and DDL statements; toggle both so the attribute has a single + * meaning. */ + if (sqlite3_db_config(H->db, SQLITE_DBCONFIG_DQS_DML, (int) bval, NULL) != SQLITE_OK + || sqlite3_db_config(H->db, SQLITE_DBCONFIG_DQS_DDL, (int) bval, NULL) != SQLITE_OK) { + return false; + } + return true; + } +#endif } return false; } diff --git a/ext/pdo_sqlite/tests/gh13597.phpt b/ext/pdo_sqlite/tests/gh13597.phpt new file mode 100644 index 000000000000..567bd6a86731 --- /dev/null +++ b/ext/pdo_sqlite/tests/gh13597.phpt @@ -0,0 +1,67 @@ +--TEST-- +GH-13597 (Allow disabling double-quoted string literals in SQLite) +--EXTENSIONS-- +pdo_sqlite +--SKIPIF-- + +--FILE-- +query($sql)->fetchColumn()); + } catch (PDOException $e) { + echo $e->getMessage(), "\n"; + } +} + +$pdo = new Pdo\Sqlite('sqlite::memory:'); +$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +echo "Enabled:\n"; +var_dump($pdo->setAttribute(Pdo\Sqlite::ATTR_DQS, true)); +var_dump($pdo->getAttribute(Pdo\Sqlite::ATTR_DQS)); +check($pdo, 'SELECT "abc"'); +$pdo->exec('CREATE TABLE t1 (a TEXT)'); +$pdo->exec('CREATE INDEX t1_idx ON t1("missing")'); +echo "index created\n"; + +echo "Disabled:\n"; +var_dump($pdo->setAttribute(Pdo\Sqlite::ATTR_DQS, false)); +var_dump($pdo->getAttribute(Pdo\Sqlite::ATTR_DQS)); +check($pdo, 'SELECT "abc"'); +check($pdo, "SELECT 'abc'"); +check($pdo, 'SELECT "a" FROM t1 UNION ALL SELECT 1'); +try { + $pdo->exec('CREATE INDEX t1_idx2 ON t1("missing")'); +} catch (PDOException $e) { + echo $e->getMessage(), "\n"; +} + +echo "Constructor option:\n"; +$pdo = new Pdo\Sqlite('sqlite::memory:', options: [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + Pdo\Sqlite::ATTR_DQS => false, +]); +var_dump($pdo->getAttribute(Pdo\Sqlite::ATTR_DQS)); +check($pdo, 'SELECT "abc"'); + +?> +--EXPECT-- +Enabled: +bool(true) +bool(true) +string(3) "abc" +index created +Disabled: +bool(true) +bool(false) +SQLSTATE[HY000]: General error: 1 no such column: abc +string(3) "abc" +int(1) +SQLSTATE[HY000]: General error: 1 no such column: missing +Constructor option: +bool(false) +SQLSTATE[HY000]: General error: 1 no such column: abc From 6bdc772c87397ca777f5dca4f5773e735127671d Mon Sep 17 00:00:00 2001 From: Juan Versolato Lopes Date: Tue, 3 Nov 2026 13:33:54 -0300 Subject: [PATCH 2/3] Accept newer SQLite wording in GH-13597 test --- ext/pdo_sqlite/tests/gh13597.phpt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/pdo_sqlite/tests/gh13597.phpt b/ext/pdo_sqlite/tests/gh13597.phpt index 567bd6a86731..fd95eaa3e69e 100644 --- a/ext/pdo_sqlite/tests/gh13597.phpt +++ b/ext/pdo_sqlite/tests/gh13597.phpt @@ -49,7 +49,7 @@ var_dump($pdo->getAttribute(Pdo\Sqlite::ATTR_DQS)); check($pdo, 'SELECT "abc"'); ?> ---EXPECT-- +--EXPECTF-- Enabled: bool(true) bool(true) @@ -58,10 +58,10 @@ index created Disabled: bool(true) bool(false) -SQLSTATE[HY000]: General error: 1 no such column: abc +SQLSTATE[HY000]: General error: 1 no such column: %Sabc%S string(3) "abc" int(1) -SQLSTATE[HY000]: General error: 1 no such column: missing +SQLSTATE[HY000]: General error: 1 no such column: %Smissing%S Constructor option: bool(false) -SQLSTATE[HY000]: General error: 1 no such column: abc +SQLSTATE[HY000]: General error: 1 no such column: %Sabc%S From bd5be74fbb9b397f3750889f14667858831447f0 Mon Sep 17 00:00:00 2001 From: Juan Versolato Lopes Date: Mon, 9 Nov 2026 14:50:00 -0300 Subject: [PATCH 3/3] Cover already prepared statements in GH-13597 test --- ext/pdo_sqlite/tests/gh13597.phpt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ext/pdo_sqlite/tests/gh13597.phpt b/ext/pdo_sqlite/tests/gh13597.phpt index fd95eaa3e69e..163bf5a922a8 100644 --- a/ext/pdo_sqlite/tests/gh13597.phpt +++ b/ext/pdo_sqlite/tests/gh13597.phpt @@ -40,6 +40,21 @@ try { echo $e->getMessage(), "\n"; } +echo "Prepared statement:\n"; +$pdo->setAttribute(Pdo\Sqlite::ATTR_DQS, true); +$stmt = $pdo->prepare('SELECT "abc"'); +$stmt->execute(); +var_dump($stmt->fetchColumn()); +$pdo->setAttribute(Pdo\Sqlite::ATTR_DQS, false); +try { + $stmt->execute(); +} catch (PDOException $e) { + echo $e->getMessage(), "\n"; +} +$pdo->setAttribute(Pdo\Sqlite::ATTR_DQS, true); +$stmt->execute(); +var_dump($stmt->fetchColumn()); + echo "Constructor option:\n"; $pdo = new Pdo\Sqlite('sqlite::memory:', options: [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, @@ -62,6 +77,10 @@ SQLSTATE[HY000]: General error: 1 no such column: %Sabc%S string(3) "abc" int(1) SQLSTATE[HY000]: General error: 1 no such column: %Smissing%S +Prepared statement: +string(3) "abc" +SQLSTATE[HY000]: General error: 1 no such column: %Sabc%S +string(3) "abc" Constructor option: bool(false) SQLSTATE[HY000]: General error: 1 no such column: %Sabc%S