From 72d1b292047cf55fbe12018b0159d4ae829f29fa Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 13 Mar 2026 18:17:40 -0400 Subject: [PATCH 1/2] Fix GH-20214: PDO::FETCH_DEFAULT unexpected behavior with PDOStatement::setFetchMode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When setFetchMode(PDO::FETCH_DEFAULT) is called, mode=0 (PDO_FETCH_USE_DEFAULT) gets stored as the statement's default fetch type. Later, do_fetch() tries to resolve PDO_FETCH_USE_DEFAULT by reading stmt->default_fetch_type, which is also 0 — circular reference that on 8.4 silently fell through to FETCH_BOTH and on master throws a ValueError. Resolve PDO_FETCH_USE_DEFAULT to the connection-level default early in pdo_stmt_setup_fetch_mode(), before flags extraction and the mode switch, so the rest of the function processes the actual fetch mode. Closes GH-20214 --- ext/pdo/pdo_stmt.c | 4 ++++ ext/pdo_sqlite/tests/gh20214.phpt | 38 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 ext/pdo_sqlite/tests/gh20214.phpt diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c index 73d4530e8f657..8ea8bac02447f 100644 --- a/ext/pdo/pdo_stmt.c +++ b/ext/pdo/pdo_stmt.c @@ -1628,6 +1628,10 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a stmt->default_fetch_type = PDO_FETCH_BOTH; + if ((mode & ~PDO_FETCH_FLAGS) == PDO_FETCH_USE_DEFAULT) { + mode = stmt->dbh->default_fetch_type; + } + flags = mode & PDO_FETCH_FLAGS; if (!pdo_verify_fetch_mode(stmt->default_fetch_type, mode, mode_arg_num, false)) { diff --git a/ext/pdo_sqlite/tests/gh20214.phpt b/ext/pdo_sqlite/tests/gh20214.phpt new file mode 100644 index 0000000000000..29adc50b0b747 --- /dev/null +++ b/ext/pdo_sqlite/tests/gh20214.phpt @@ -0,0 +1,38 @@ +--TEST-- +GH-20214 (PDO::FETCH_DEFAULT unexpected behavior with PDOStatement::setFetchMode) +--EXTENSIONS-- +pdo_sqlite +--FILE-- +setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); + +// setFetchMode with FETCH_DEFAULT should use connection default (FETCH_OBJ) +$stmt = $db->query("SELECT 'v1' AS c1, 'v2' AS c2"); +$stmt->setFetchMode(PDO::FETCH_DEFAULT); +$row = $stmt->fetch(); +var_dump($row instanceof stdClass); +var_dump($row->c1); + +// fetch with FETCH_DEFAULT should also use connection default +$stmt = $db->query("SELECT 'v1' AS c1, 'v2' AS c2"); +$row = $stmt->fetch(PDO::FETCH_DEFAULT); +var_dump($row instanceof stdClass); + +// fetchAll with FETCH_DEFAULT should also use connection default +$stmt = $db->query("SELECT 'v1' AS c1, 'v2' AS c2"); +$rows = $stmt->fetchAll(PDO::FETCH_DEFAULT); +var_dump($rows[0] instanceof stdClass); + +// setFetchMode then fetch without argument +$stmt = $db->query("SELECT 'v1' AS c1, 'v2' AS c2"); +$stmt->setFetchMode(PDO::FETCH_DEFAULT); +$row = $stmt->fetch(); +var_dump($row instanceof stdClass); +?> +--EXPECT-- +bool(true) +string(2) "v1" +bool(true) +bool(true) +bool(true) From 38514c1a9c06f92e4c166828d48ebfd7705781c6 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 15 Mar 2026 11:44:12 -0400 Subject: [PATCH 2/2] ext/pdo_sqlite: add ATTR_SQL and ATTR_EXPANDED_SQL statement attributes Add support for retrieving the SQL text of a prepared statement via PDOStatement::getAttribute(), mirroring SQLite3Stmt::getSQL(). Pdo\Sqlite::ATTR_SQL - original SQL text (sqlite3_sql) Pdo\Sqlite::ATTR_EXPANDED_SQL - SQL with bound parameters expanded (sqlite3_expanded_sql) Closes GH-21322 --- ext/pdo_sqlite/config.m4 | 7 +++++ ext/pdo_sqlite/config.w32 | 1 + ext/pdo_sqlite/pdo_sqlite.stub.php | 8 +++++ ext/pdo_sqlite/pdo_sqlite_arginfo.h | 16 +++++++++- ext/pdo_sqlite/php_pdo_sqlite_int.h | 4 ++- ext/pdo_sqlite/sqlite_statement.c | 26 +++++++++++++++++ .../subclasses/pdo_sqlite_constants.phpt | 2 ++ .../subclasses/pdo_sqlite_getattr_sql.phpt | 29 +++++++++++++++++++ 8 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 ext/pdo_sqlite/tests/subclasses/pdo_sqlite_getattr_sql.phpt diff --git a/ext/pdo_sqlite/config.m4 b/ext/pdo_sqlite/config.m4 index 7b7700481a30d..9a037d58903e6 100644 --- a/ext/pdo_sqlite/config.m4 +++ b/ext/pdo_sqlite/config.m4 @@ -17,6 +17,13 @@ if test "$PHP_PDO_SQLITE" != "no"; then [], [$PDO_SQLITE_SHARED_LIBADD]) + PHP_CHECK_LIBRARY([sqlite3], [sqlite3_expanded_sql], + [AC_DEFINE([HAVE_SQLITE3_EXPANDED_SQL], [1], + [Define to 1 if SQLite library has the 'sqlite3_expanded_sql' + function.])], + [], + [$PDO_SQLITE_SHARED_LIBADD]) + PHP_CHECK_LIBRARY([sqlite3], [sqlite3_load_extension], [], [AC_DEFINE([PDO_SQLITE_OMIT_LOAD_EXTENSION], [1], diff --git a/ext/pdo_sqlite/config.w32 b/ext/pdo_sqlite/config.w32 index d6162278abedf..76ee5598fa3ef 100644 --- a/ext/pdo_sqlite/config.w32 +++ b/ext/pdo_sqlite/config.w32 @@ -8,6 +8,7 @@ if (PHP_PDO_SQLITE != "no") { ADD_EXTENSION_DEP('pdo_sqlite', 'pdo'); AC_DEFINE("HAVE_SQLITE3_COLUMN_TABLE_NAME", 1, "Define to 1 if SQLite library was compiled with the SQLITE_ENABLE_COLUMN_METADATA and has the 'sqlite3_column_table_name' function."); + AC_DEFINE("HAVE_SQLITE3_EXPANDED_SQL", 1, "Define to 1 if SQLite library has the 'sqlite3_expanded_sql' function."); ADD_MAKEFILE_FRAGMENT(); } else { WARNING("pdo_sqlite not enabled; libraries and/or headers not found"); diff --git a/ext/pdo_sqlite/pdo_sqlite.stub.php b/ext/pdo_sqlite/pdo_sqlite.stub.php index 53f1ceba427b0..4cbb758d1e43e 100644 --- a/ext/pdo_sqlite/pdo_sqlite.stub.php +++ b/ext/pdo_sqlite/pdo_sqlite.stub.php @@ -42,6 +42,14 @@ class Sqlite extends \PDO /** @cvalue PDO_SQLITE_ATTR_TRANSACTION_MODE */ public const int ATTR_TRANSACTION_MODE = UNKNOWN; + /** @cvalue PDO_SQLITE_ATTR_SQL */ + public const int ATTR_SQL = UNKNOWN; + +#ifdef HAVE_SQLITE3_EXPANDED_SQL + /** @cvalue PDO_SQLITE_ATTR_EXPANDED_SQL */ + public const int ATTR_EXPANDED_SQL = 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 73a9158301c0a..f9cadf4faa5e6 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: c16b75eca0adbce4f98b304b1264dd62e37267ff */ 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) @@ -128,6 +128,20 @@ static zend_class_entry *register_class_Pdo_Sqlite(zend_class_entry *class_entry 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); + zval const_ATTR_SQL_value; + ZVAL_LONG(&const_ATTR_SQL_value, PDO_SQLITE_ATTR_SQL); + zend_string *const_ATTR_SQL_name = zend_string_init_interned("ATTR_SQL", sizeof("ATTR_SQL") - 1, true); + zend_declare_typed_class_constant(class_entry, const_ATTR_SQL_name, &const_ATTR_SQL_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release_ex(const_ATTR_SQL_name, true); +#if defined(HAVE_SQLITE3_EXPANDED_SQL) + + zval const_ATTR_EXPANDED_SQL_value; + ZVAL_LONG(&const_ATTR_EXPANDED_SQL_value, PDO_SQLITE_ATTR_EXPANDED_SQL); + zend_string *const_ATTR_EXPANDED_SQL_name = zend_string_init_interned("ATTR_EXPANDED_SQL", sizeof("ATTR_EXPANDED_SQL") - 1, true); + zend_declare_typed_class_constant(class_entry, const_ATTR_EXPANDED_SQL_name, &const_ATTR_EXPANDED_SQL_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release_ex(const_ATTR_EXPANDED_SQL_name, true); +#endif + 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, true); diff --git a/ext/pdo_sqlite/php_pdo_sqlite_int.h b/ext/pdo_sqlite/php_pdo_sqlite_int.h index 0cb09cfaa4fb4..b23eb62534d48 100644 --- a/ext/pdo_sqlite/php_pdo_sqlite_int.h +++ b/ext/pdo_sqlite/php_pdo_sqlite_int.h @@ -77,7 +77,9 @@ 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_SQL, + PDO_SQLITE_ATTR_EXPANDED_SQL }; typedef int pdo_sqlite_create_collation_callback(void*, int, const void*, int, const void*); diff --git a/ext/pdo_sqlite/sqlite_statement.c b/ext/pdo_sqlite/sqlite_statement.c index ffb8c1ad4b3c8..8eff6db0f7f0f 100644 --- a/ext/pdo_sqlite/sqlite_statement.c +++ b/ext/pdo_sqlite/sqlite_statement.c @@ -408,6 +408,32 @@ static int pdo_sqlite_stmt_get_attribute(pdo_stmt_t *stmt, zend_long attr, zval zend_value_error("explain statement unsupported"); return 0; #endif + case PDO_SQLITE_ATTR_SQL: { + const char *sql = sqlite3_sql(S->stmt); + if (sql) { + ZVAL_STRING(val, sql); + } else { + ZVAL_NULL(val); + } + return 1; + } + + case PDO_SQLITE_ATTR_EXPANDED_SQL: { +#ifdef HAVE_SQLITE3_EXPANDED_SQL + char *sql = sqlite3_expanded_sql(S->stmt); + if (sql) { + ZVAL_STRING(val, sql); + sqlite3_free(sql); + } else { + ZVAL_NULL(val); + } + return 1; +#else + zend_value_error("expanded sql unsupported"); + return -1; +#endif + } + default: return 0; } diff --git a/ext/pdo_sqlite/tests/subclasses/pdo_sqlite_constants.phpt b/ext/pdo_sqlite/tests/subclasses/pdo_sqlite_constants.phpt index d1db58b1323eb..0abe6f65d6acb 100644 --- a/ext/pdo_sqlite/tests/subclasses/pdo_sqlite_constants.phpt +++ b/ext/pdo_sqlite/tests/subclasses/pdo_sqlite_constants.phpt @@ -14,6 +14,7 @@ var_dump(Pdo\Sqlite::OPEN_CREATE); var_dump(Pdo\Sqlite::ATTR_READONLY_STATEMENT); var_dump(Pdo\Sqlite::ATTR_EXTENDED_RESULT_CODES); var_dump(Pdo\Sqlite::ATTR_BUSY_STATEMENT); +var_dump(Pdo\Sqlite::ATTR_SQL); ?> --EXPECTF-- @@ -26,3 +27,4 @@ int(%d) int(%d) int(%d) int(%d) +int(%d) diff --git a/ext/pdo_sqlite/tests/subclasses/pdo_sqlite_getattr_sql.phpt b/ext/pdo_sqlite/tests/subclasses/pdo_sqlite_getattr_sql.phpt new file mode 100644 index 0000000000000..885a523fed49d --- /dev/null +++ b/ext/pdo_sqlite/tests/subclasses/pdo_sqlite_getattr_sql.phpt @@ -0,0 +1,29 @@ +--TEST-- +Pdo\Sqlite::ATTR_SQL and Pdo\Sqlite::ATTR_EXPANDED_SQL usage +--EXTENSIONS-- +pdo_sqlite +--SKIPIF-- + +--FILE-- +prepare('SELECT :name AS greeting, :num AS number'); +var_dump($stmt->getAttribute(Pdo\Sqlite::ATTR_SQL)); +var_dump($stmt->getAttribute(Pdo\Sqlite::ATTR_EXPANDED_SQL)); + +$stmt->bindValue(':name', 'hello world'); +$stmt->bindValue(':num', 42, PDO::PARAM_INT); +$stmt->execute(); + +var_dump($stmt->getAttribute(Pdo\Sqlite::ATTR_SQL)); +var_dump($stmt->getAttribute(Pdo\Sqlite::ATTR_EXPANDED_SQL)); +?> +--EXPECT-- +string(40) "SELECT :name AS greeting, :num AS number" +string(39) "SELECT NULL AS greeting, NULL AS number" +string(40) "SELECT :name AS greeting, :num AS number" +string(46) "SELECT 'hello world' AS greeting, 42 AS number"