From 8966f72f0819d82d73ed9795b5fc9f21afa3ae2f Mon Sep 17 00:00:00 2001 From: Eric Norris Date: Thu, 23 Apr 2026 15:17:49 -0400 Subject: [PATCH 1/9] feat(mysqlnd): send COM_RESET_CONNECTION in restart_psession Before this commit, persistent MySQL connection reuse relied on COM_CHANGE_USER to reset server-side session state in mysqli. PDO MySQL did not reset server-side state at all; it only pinged to check liveness, leaving user variables, temporary tables, and transaction state from previous requests intact. After this commit, both mysqli and PDO MySQL send COM_RESET_CONNECTION when reusing a persistent connection, which resets all server-side session state without re-authentication. We've implemented this by adding a reset_connection command to mysqlnd's command layer. It's similar to the existing ping command, since there's no payload. We now call it from restart_psession, which we've also changed to return enum_func_status so callers can handle failures. In mysqli, we've replaced the COM_CHANGE_USER call in the persistent reuse path with a call to restart_psession. In PDO MySQL, we've replaced the mysql_ping in check_liveness with a call to restart_psession, which both verifies the connection is alive and resets session state in a single round trip. We've also removed a call to restart_psession on unconnected handles during initialization. --- ext/mysqli/mysqli_mysqlnd.h | 1 - ext/mysqli/mysqli_nonapi.c | 8 +-- ext/mysqli/tests/mysqli_pconnect_reset.phpt | 61 +++++++++++++++++++++ ext/mysqlnd/mysqlnd_commands.c | 28 ++++++++++ ext/mysqlnd/mysqlnd_connection.c | 15 +++-- ext/mysqlnd/mysqlnd_structs.h | 4 +- ext/pdo_mysql/mysql_driver.c | 11 ++-- 7 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 ext/mysqli/tests/mysqli_pconnect_reset.phpt diff --git a/ext/mysqli/mysqli_mysqlnd.h b/ext/mysqli/mysqli_mysqlnd.h index b15644c8e1f6..4d91c1b289d3 100644 --- a/ext/mysqli/mysqli_mysqlnd.h +++ b/ext/mysqli/mysqli_mysqlnd.h @@ -37,6 +37,5 @@ #define mysqli_stmt_close(c, implicit) mysqlnd_stmt_close((c), (implicit)) #define mysqli_free_result(r, implicit) mysqlnd_free_result((r), (implicit)) #define mysqli_async_query(c, q, l) mysqlnd_async_query((c), (q), (l)) -#define mysqli_change_user_silent(c, u, p, d, p_len) mysqlnd_change_user_ex((c), (u), (p), (d), true, (size_t)(p_len)) #endif diff --git a/ext/mysqli/mysqli_nonapi.c b/ext/mysqli/mysqli_nonapi.c index 1e46aeedd93b..e4e53f68009b 100644 --- a/ext/mysqli/mysqli_nonapi.c +++ b/ext/mysqli/mysqli_nonapi.c @@ -173,14 +173,8 @@ void mysqli_common_connect(INTERNAL_FUNCTION_PARAMETERS, bool is_real_connect, b mysql->mysql = zend_ptr_stack_pop(&plist->free_links); MyG(num_inactive_persistent)--; - /* reset variables */ -#ifndef MYSQLI_NO_CHANGE_USER_ON_PCONNECT - if (!mysqli_change_user_silent(mysql->mysql, username, passwd, dbname, passwd_len)) { -#else - if (!mysql_ping(mysql->mysql)) { -#endif - mysqlnd_restart_psession(mysql->mysql); + if (!mysqlnd_restart_psession(mysql->mysql)) { MyG(num_active_persistent)++; /* clear error */ diff --git a/ext/mysqli/tests/mysqli_pconnect_reset.phpt b/ext/mysqli/tests/mysqli_pconnect_reset.phpt new file mode 100644 index 000000000000..a6aa9c4a00ec --- /dev/null +++ b/ext/mysqli/tests/mysqli_pconnect_reset.phpt @@ -0,0 +1,61 @@ +--TEST-- +mysqli_pconnect() - COM_RESET_CONNECTION clears session state +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- + +--EXPECT-- +done! diff --git a/ext/mysqlnd/mysqlnd_commands.c b/ext/mysqlnd/mysqlnd_commands.c index 1bb38be089e6..041ea57742de 100644 --- a/ext/mysqlnd/mysqlnd_commands.c +++ b/ext/mysqlnd/mysqlnd_commands.c @@ -143,6 +143,33 @@ MYSQLND_METHOD(mysqlnd_command, ping)(MYSQLND_CONN_DATA * const conn) /* }}} */ +/* {{{ mysqlnd_command::reset_connection */ +static enum_func_status +MYSQLND_METHOD(mysqlnd_command, reset_connection)(MYSQLND_CONN_DATA * const conn) +{ + const func_mysqlnd_protocol_payload_decoder_factory__send_command send_command = conn->payload_decoder_factory->m.send_command; + const func_mysqlnd_protocol_payload_decoder_factory__send_command_handle_response send_command_handle_response = conn->payload_decoder_factory->m.send_command_handle_response; + enum_func_status ret = FAIL; + + DBG_ENTER("mysqlnd_command::reset_connection"); + + ret = send_command(conn->payload_decoder_factory, COM_RESET_CONNECTION, NULL, 0, FALSE, + &conn->state, + conn->error_info, + conn->upsert_status, + conn->stats, + conn->m->send_close, + conn); + if (PASS == ret) { + ret = send_command_handle_response(conn->payload_decoder_factory, PROT_OK_PACKET, FALSE, COM_RESET_CONNECTION, FALSE, + conn->error_info, conn->upsert_status, &conn->last_message); + } + + DBG_RETURN(ret); +} +/* }}} */ + + /* {{{ mysqlnd_command::statistics */ static enum_func_status MYSQLND_METHOD(mysqlnd_command, statistics)(MYSQLND_CONN_DATA * const conn, zend_string ** message) @@ -659,4 +686,5 @@ MYSQLND_CLASS_METHODS_START(mysqlnd_command) MYSQLND_METHOD(mysqlnd_command, stmt_close), MYSQLND_METHOD(mysqlnd_command, enable_ssl), MYSQLND_METHOD(mysqlnd_command, handshake), + MYSQLND_METHOD(mysqlnd_command, reset_connection), MYSQLND_CLASS_METHODS_END; diff --git a/ext/mysqlnd/mysqlnd_connection.c b/ext/mysqlnd/mysqlnd_connection.c index e3ca87bd7131..49e176de7d4a 100644 --- a/ext/mysqlnd/mysqlnd_connection.c +++ b/ext/mysqlnd/mysqlnd_connection.c @@ -354,14 +354,19 @@ MYSQLND_METHOD(mysqlnd_conn_data, set_server_option)(MYSQLND_CONN_DATA * const c /* {{{ mysqlnd_conn_data::restart_psession */ -static void +static enum_func_status MYSQLND_METHOD(mysqlnd_conn_data, restart_psession)(MYSQLND_CONN_DATA * conn) { DBG_ENTER("mysqlnd_conn_data::restart_psession"); - MYSQLND_INC_CONN_STATISTIC(conn->stats, STAT_CONNECT_REUSED); - conn->current_result = NULL; - conn->last_message.s = NULL; - DBG_VOID_RETURN; + + enum_func_status ret = conn->command->reset_connection(conn); + if (ret == PASS) { + MYSQLND_INC_CONN_STATISTIC(conn->stats, STAT_CONNECT_REUSED); + conn->current_result = NULL; + conn->last_message.s = NULL; + } + + DBG_RETURN(ret); } /* }}} */ diff --git a/ext/mysqlnd/mysqlnd_structs.h b/ext/mysqlnd/mysqlnd_structs.h index 80d0abfcae6f..e5a4960ebe98 100644 --- a/ext/mysqlnd/mysqlnd_structs.h +++ b/ext/mysqlnd/mysqlnd_structs.h @@ -306,6 +306,7 @@ typedef enum_func_status (*func_mysqlnd_execute_com_set_option)(MYSQLND_CONN_DAT typedef enum_func_status (*func_mysqlnd_execute_com_debug)(MYSQLND_CONN_DATA * const conn); typedef enum_func_status (*func_mysqlnd_execute_com_init_db)(MYSQLND_CONN_DATA * const conn, const MYSQLND_CSTRING db); typedef enum_func_status (*func_mysqlnd_execute_com_ping)(MYSQLND_CONN_DATA * const conn); +typedef enum_func_status (*func_mysqlnd_execute_com_reset_connection)(MYSQLND_CONN_DATA * const conn); typedef enum_func_status (*func_mysqlnd_execute_com_statistics)(MYSQLND_CONN_DATA * const conn, zend_string ** message); typedef enum_func_status (*func_mysqlnd_execute_com_process_kill)(MYSQLND_CONN_DATA * const conn, const unsigned int process_id, const bool read_response); typedef enum_func_status (*func_mysqlnd_execute_com_refresh)(MYSQLND_CONN_DATA * const conn, const uint8_t options); @@ -344,6 +345,7 @@ MYSQLND_CLASS_METHODS_TYPE(mysqlnd_command) func_mysqlnd_execute_com_stmt_close stmt_close; func_mysqlnd_execute_com_enable_ssl enable_ssl; func_mysqlnd_execute_com_handshake handshake; + func_mysqlnd_execute_com_reset_connection reset_connection; }; @@ -482,7 +484,7 @@ typedef enum_func_status (*func_mysqlnd_conn_data__free_reference)(MYSQLND_CONN_ typedef enum_func_status (*func_mysqlnd_conn_data__send_command_do_request)(MYSQLND_CONN_DATA * const conn, const enum php_mysqlnd_server_command command, const zend_uchar * const arg, const size_t arg_len, const bool silent, const bool ignore_upsert_status); typedef enum_func_status (*func_mysqlnd_conn_data__send_command_handle_response)(MYSQLND_CONN_DATA * const conn, const enum mysqlnd_packet_type ok_packet, const bool silent, const enum php_mysqlnd_server_command command, const bool ignore_upsert_status); -typedef void (*func_mysqlnd_conn_data__restart_psession)(MYSQLND_CONN_DATA * conn); +typedef enum_func_status (*func_mysqlnd_conn_data__restart_psession)(MYSQLND_CONN_DATA * conn); typedef void (*func_mysqlnd_conn_data__end_psession)(MYSQLND_CONN_DATA * conn); typedef enum_func_status (*func_mysqlnd_conn_data__send_close)(MYSQLND_CONN_DATA * conn); diff --git a/ext/pdo_mysql/mysql_driver.c b/ext/pdo_mysql/mysql_driver.c index 54a8803971ca..086ebc6d19e9 100644 --- a/ext/pdo_mysql/mysql_driver.c +++ b/ext/pdo_mysql/mysql_driver.c @@ -603,9 +603,15 @@ static zend_result pdo_mysql_check_liveness(pdo_dbh_t *dbh) PDO_DBG_ENTER("pdo_mysql_check_liveness"); PDO_DBG_INF_FMT("dbh=%p", dbh); +#ifdef PDO_USE_MYSQLND + if (mysqlnd_restart_psession(H->server)) { + PDO_DBG_RETURN(FAILURE); + } +#else if (mysql_ping(H->server)) { PDO_DBG_RETURN(FAILURE); } +#endif PDO_DBG_RETURN(SUCCESS); } /* }}} */ @@ -724,11 +730,6 @@ static int pdo_mysql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) pdo_mysql_error(dbh); goto cleanup; } -#ifdef PDO_USE_MYSQLND - if (dbh->is_persistent) { - mysqlnd_restart_psession(H->server); - } -#endif dbh->driver_data = H; From 49742e6e9c28006dc6bcf9b554ff91f317d6c47d Mon Sep 17 00:00:00 2001 From: Eric Norris Date: Thu, 25 Jun 2026 14:11:16 +0100 Subject: [PATCH 2/9] use a dedicated reset hook instead of overloading check_liveness The initial approach reset PDO MySQL persistent connections by sending COM_RESET_CONNECTION from check_liveness. This should have been safe because check_liveness is only ever used by the persistent connection code, but it is somewhat confusing from a behavior perspective - why should a liveness check actually mutate the connection? Instead, I've added a dedicated reset_connection hook to the driver method table (pdo_dbh_methods). On persistent reuse, PDO core calls reset_connection when the driver provides it, and falls back to check_liveness (the existing behavior) otherwise. A successful reset also confirms liveness, so the two are never both invoked. PDO MySQL implements the hook (mysqlnd builds only) via restart_psession, and check_liveness is restored to its plain ping. The other bundled PDO drivers leave the hook NULL and keep their existing behavior. --- ext/pdo/pdo_dbh.c | 13 ++++- ext/pdo/php_pdo_driver.h | 12 ++++- ext/pdo_dblib/dblib_driver.c | 1 + ext/pdo_firebird/firebird_driver.c | 1 + ext/pdo_mysql/mysql_driver.c | 30 +++++++++-- .../tests/pdo_mysql_pconnect_reset.phpt | 54 +++++++++++++++++++ ext/pdo_odbc/odbc_driver.c | 1 + ext/pdo_pgsql/pgsql_driver.c | 1 + ext/pdo_sqlite/sqlite_driver.c | 1 + 9 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 ext/pdo_mysql/tests/pdo_mysql_pconnect_reset.phpt diff --git a/ext/pdo/pdo_dbh.c b/ext/pdo/pdo_dbh.c index 21002ce3a93d..2a09befd28f7 100644 --- a/ext/pdo/pdo_dbh.c +++ b/ext/pdo/pdo_dbh.c @@ -414,8 +414,17 @@ PDO_API void php_pdo_internal_construct_driver(INTERNAL_FUNCTION_PARAMETERS, zen if (le->type == php_pdo_list_entry()) { pdbh = (pdo_dbh_t*)le->ptr; - /* is the connection still alive ? */ - if (pdbh->methods->check_liveness && FAILURE == (pdbh->methods->check_liveness)(pdbh)) { + /* Reset/validate the pooled connection before reuse. A + * driver-provided reset_connection both clears server-side + * session state and confirms liveness; otherwise fall back + * to a plain liveness check. */ + zend_result alive = SUCCESS; + if (pdbh->methods->reset_connection) { + alive = (pdbh->methods->reset_connection)(pdbh); + } else if (pdbh->methods->check_liveness) { + alive = (pdbh->methods->check_liveness)(pdbh); + } + if (FAILURE == alive) { /* nope... need to kill it */ pdbh->refcount--; zend_list_close(le); diff --git a/ext/pdo/php_pdo_driver.h b/ext/pdo/php_pdo_driver.h index 9dc18f75bfe1..9b9486759cc0 100644 --- a/ext/pdo/php_pdo_driver.h +++ b/ext/pdo/php_pdo_driver.h @@ -32,7 +32,7 @@ struct pdo_bound_param_data; # define FALSE 0 #endif -#define PDO_DRIVER_API 20240423 +#define PDO_DRIVER_API 20260502 /* Doctrine hardcodes these constants, avoid changing their values. */ enum pdo_param_type { @@ -268,6 +268,15 @@ typedef int (*pdo_dbh_get_attr_func)(pdo_dbh_t *dbh, zend_long attr, zval *val); * You may set this handler to NULL, which is equivalent to returning SUCCESS. */ typedef zend_result (*pdo_dbh_check_liveness_func)(pdo_dbh_t *dbh); +/* called when a persistent connection is pulled from the pool for reuse, to + * reset any server-side session state left over from the previous request. + * Return SUCCESS if the connection was reset and is ready for reuse, FAILURE + * otherwise (PDO will then discard it). A successful reset also implies the + * connection is alive, so when this handler is present PDO does not also call + * check_liveness. You may set this handler to NULL, in which case PDO falls + * back to check_liveness. */ +typedef zend_result (*pdo_dbh_reset_connection_func)(pdo_dbh_t *dbh); + /* called at request end for each persistent dbh; this gives the driver * the opportunity to safely release resources that only have per-request * scope */ @@ -307,6 +316,7 @@ struct pdo_dbh_methods { pdo_dbh_fetch_error_func fetch_err; pdo_dbh_get_attr_func get_attribute; pdo_dbh_check_liveness_func check_liveness; + pdo_dbh_reset_connection_func reset_connection; pdo_dbh_get_driver_methods_func get_driver_methods; pdo_dbh_request_shutdown persistent_shutdown; /* if defined to NULL, PDO will use its internal transaction tracking state */ diff --git a/ext/pdo_dblib/dblib_driver.c b/ext/pdo_dblib/dblib_driver.c index 42ba72b40ede..ea95975a2c39 100644 --- a/ext/pdo_dblib/dblib_driver.c +++ b/ext/pdo_dblib/dblib_driver.c @@ -433,6 +433,7 @@ static const struct pdo_dbh_methods dblib_methods = { dblib_fetch_error, /* fetch error */ dblib_get_attribute, /* get attr */ NULL, /* check liveness */ + NULL, /* reset_connection */ NULL, /* get driver methods */ NULL, /* request shutdown */ NULL, /* in transaction, use PDO's internal tracking mechanism */ diff --git a/ext/pdo_firebird/firebird_driver.c b/ext/pdo_firebird/firebird_driver.c index 8193132beaf4..063193551b9b 100644 --- a/ext/pdo_firebird/firebird_driver.c +++ b/ext/pdo_firebird/firebird_driver.c @@ -1316,6 +1316,7 @@ static const struct pdo_dbh_methods firebird_methods = { /* {{{ */ pdo_firebird_fetch_error_func, pdo_firebird_get_attribute, pdo_firebird_check_liveness, + NULL, /* reset_connection */ NULL, /* get driver methods */ NULL, /* request shutdown */ pdo_firebird_in_manually_transaction, diff --git a/ext/pdo_mysql/mysql_driver.c b/ext/pdo_mysql/mysql_driver.c index 086ebc6d19e9..7acc6ec821a2 100644 --- a/ext/pdo_mysql/mysql_driver.c +++ b/ext/pdo_mysql/mysql_driver.c @@ -603,18 +603,33 @@ static zend_result pdo_mysql_check_liveness(pdo_dbh_t *dbh) PDO_DBG_ENTER("pdo_mysql_check_liveness"); PDO_DBG_INF_FMT("dbh=%p", dbh); -#ifdef PDO_USE_MYSQLND - if (mysqlnd_restart_psession(H->server)) { + if (mysql_ping(H->server)) { PDO_DBG_RETURN(FAILURE); } -#else - if (mysql_ping(H->server)) { + PDO_DBG_RETURN(SUCCESS); +} +/* }}} */ + +#ifdef PDO_USE_MYSQLND +/* {{{ pdo_mysql_reset_connection */ +static zend_result pdo_mysql_reset_connection(pdo_dbh_t *dbh) +{ + pdo_mysql_db_handle *H = (pdo_mysql_db_handle *)dbh->driver_data; + + PDO_DBG_ENTER("pdo_mysql_reset_connection"); + PDO_DBG_INF_FMT("dbh=%p", dbh); + + /* Reset the server-side session state (user variables, temporary tables, + * transaction state, ...) of the persistent connection being reused. This + * also doubles as a liveness check: a failure means the connection can no + * longer be used. */ + if (mysqlnd_restart_psession(H->server) != PASS) { PDO_DBG_RETURN(FAILURE); } -#endif PDO_DBG_RETURN(SUCCESS); } /* }}} */ +#endif /* {{{ pdo_mysql_request_shutdown */ static void pdo_mysql_request_shutdown(pdo_dbh_t *dbh) @@ -660,6 +675,11 @@ static const struct pdo_dbh_methods mysql_methods = { pdo_mysql_fetch_error_func, pdo_mysql_get_attribute, pdo_mysql_check_liveness, +#ifdef PDO_USE_MYSQLND + pdo_mysql_reset_connection, +#else + NULL, /* reset_connection */ +#endif NULL, pdo_mysql_request_shutdown, pdo_mysql_in_transaction, diff --git a/ext/pdo_mysql/tests/pdo_mysql_pconnect_reset.phpt b/ext/pdo_mysql/tests/pdo_mysql_pconnect_reset.phpt new file mode 100644 index 000000000000..b86d85f21f15 --- /dev/null +++ b/ext/pdo_mysql/tests/pdo_mysql_pconnect_reset.phpt @@ -0,0 +1,54 @@ +--TEST-- +PDO MySQL persistent connection session state is reset (COM_RESET_CONNECTION) on reuse +--EXTENSIONS-- +pdo_mysql +--SKIPIF-- + +--FILE-- + true]); +$db1->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); + +$con1 = $db1->query('SELECT CONNECTION_ID()')->fetchColumn(); + +/* leave some server-side session state behind */ +$db1->exec('SET @test_var = 42'); +$db1->exec('CREATE TEMPORARY TABLE pdo_reset_tmp (id INT)'); + +$tmp = $db1->query('SELECT @test_var')->fetchColumn(); +if ($tmp != 42) + printf("[001] Expected 42, got %s\n", var_export($tmp, true)); + +/* release the handle; the persistent connection stays pooled */ +$db1 = null; + +/* reusing the pooled connection must reset its session state */ +$db2 = new PDO($dsn, $user, $pass, [PDO::ATTR_PERSISTENT => true]); +$db2->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); + +$con2 = $db2->query('SELECT CONNECTION_ID()')->fetchColumn(); +if ($con1 != $con2) + printf("[002] Expected the pooled connection to be reused (%s vs %s)\n", + var_export($con1, true), var_export($con2, true)); + +$tmp = $db2->query('SELECT @test_var')->fetchColumn(); +if (null !== $tmp) + printf("[003] User variable should have been reset, got %s\n", var_export($tmp, true)); + +$stmt = $db2->query("SHOW TABLES LIKE 'pdo_reset_tmp'"); +if (false !== $stmt->fetch()) + printf("[004] Temporary table should not exist after reset\n"); + +echo "done!"; +?> +--EXPECT-- +done! diff --git a/ext/pdo_odbc/odbc_driver.c b/ext/pdo_odbc/odbc_driver.c index 4c627419d18e..dca4170279f0 100644 --- a/ext/pdo_odbc/odbc_driver.c +++ b/ext/pdo_odbc/odbc_driver.c @@ -461,6 +461,7 @@ static const struct pdo_dbh_methods odbc_methods = { pdo_odbc_fetch_error_func, odbc_handle_get_attr, /* get attr */ odbc_handle_check_liveness, /* check_liveness */ + NULL, /* reset_connection */ NULL, /* get_driver_methods */ NULL, /* request_shutdown */ NULL, /* in transaction, use PDO's internal tracking mechanism */ diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c index 3cfb7a3fa01a..786d72e4118b 100644 --- a/ext/pdo_pgsql/pgsql_driver.c +++ b/ext/pdo_pgsql/pgsql_driver.c @@ -1390,6 +1390,7 @@ static const struct pdo_dbh_methods pgsql_methods = { pdo_pgsql_fetch_error_func, pdo_pgsql_get_attribute, pdo_pgsql_check_liveness, /* check_liveness */ + NULL, /* reset_connection */ pdo_pgsql_get_driver_methods, /* get_driver_methods */ pdo_pgsql_request_shutdown, pgsql_handle_in_transaction, diff --git a/ext/pdo_sqlite/sqlite_driver.c b/ext/pdo_sqlite/sqlite_driver.c index 39efa02dd583..8f6df4185dd2 100644 --- a/ext/pdo_sqlite/sqlite_driver.c +++ b/ext/pdo_sqlite/sqlite_driver.c @@ -783,6 +783,7 @@ static const struct pdo_dbh_methods sqlite_methods = { pdo_sqlite_fetch_error_func, pdo_sqlite_get_attribute, NULL, /* check_liveness: not needed */ + NULL, /* reset_connection */ get_driver_methods, pdo_sqlite_request_shutdown, pdo_sqlite_in_transaction, From 63fd772aa7bcd9aa515110752f6aab2b263a8026 Mon Sep 17 00:00:00 2001 From: Eric Norris Date: Fri, 3 Jul 2026 11:25:33 -0400 Subject: [PATCH 3/9] fix(pdo): only reset idle persistent connections, close discarded ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A persistent PDO connection can be shared by several open handles in the same request: two `new PDO(sameDSN, [PERSISTENT])` resolve to one underlying connection. Resetting it every time a handle is constructed would wipe session state — open transactions, temporary tables, user variables — out from under a handle that is still using it. This commit changes the persistent connection code to gate the reset on the connection being idle: when its refcount is 1 we hold the only reference, so it is safe to reset (which also confirms liveness) and, on failure, discard it. Note that previously the code only discarded the connection by marking the resource closed and decrementing the refcount — nothing ever ran the closer — and thus the PDO handle, and potentially the connection, leaked. Now, we actually remove the connection from the persistent list, so we both close the connection (if necessary) and ensure that the handle is cleaned up. If the refcount is greater than 1, we follow the existing behavior of only running a liveness check. --- ext/mysqli/tests/mysqli_pconnect_reset.phpt | 5 +- ext/pdo/pdo_dbh.c | 39 ++++++++++----- .../tests/pdo_mysql_pconnect_reset.phpt | 4 +- .../pdo_mysql_pconnect_shared_no_reset.phpt | 49 +++++++++++++++++++ 4 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 ext/pdo_mysql/tests/pdo_mysql_pconnect_shared_no_reset.phpt diff --git a/ext/mysqli/tests/mysqli_pconnect_reset.phpt b/ext/mysqli/tests/mysqli_pconnect_reset.phpt index a6aa9c4a00ec..c5cc7fbb1cb2 100644 --- a/ext/mysqli/tests/mysqli_pconnect_reset.phpt +++ b/ext/mysqli/tests/mysqli_pconnect_reset.phpt @@ -47,11 +47,10 @@ if ($row['v'] !== null) { } mysqli_free_result($res); -$res = mysqli_query($link, "SHOW TABLES LIKE 'test_reset_tmp'"); -if (mysqli_num_rows($res) !== 0) { +/* SHOW TABLES never lists temporary tables, so probe by selecting from it */ +if (false !== @mysqli_query($link, 'SELECT COUNT(*) FROM test_reset_tmp')) { printf("[006] Temporary table should not exist after reset\n"); } -mysqli_free_result($res); mysqli_close($link); diff --git a/ext/pdo/pdo_dbh.c b/ext/pdo/pdo_dbh.c index dd6d6f687aa7..dc046c347a08 100644 --- a/ext/pdo/pdo_dbh.c +++ b/ext/pdo/pdo_dbh.c @@ -414,18 +414,33 @@ PDO_API void php_pdo_internal_construct_driver(INTERNAL_FUNCTION_PARAMETERS, zen if (le->type == php_pdo_list_entry()) { pdbh = (pdo_dbh_t*)le->ptr; - /* Reset/validate the pooled connection before reuse. A - * driver-provided reset_connection both clears server-side - * session state and confirms liveness; otherwise fall back - * to a plain liveness check. */ - zend_result alive = SUCCESS; - if (pdbh->methods->reset_connection) { - alive = (pdbh->methods->reset_connection)(pdbh); - } else if (pdbh->methods->check_liveness) { - alive = (pdbh->methods->check_liveness)(pdbh); - } - if (FAILURE == alive) { - /* nope... need to kill it */ + if (pdbh->refcount == 1) { + /* refcount 1 means no other open handle is using this + * pooled connection, so we can reset its server-side + * session state before handing it out. reset_connection + * doubles as a liveness check; drivers without it fall + * back to check_liveness. */ + zend_result alive = SUCCESS; + if (pdbh->methods->reset_connection) { + alive = (pdbh->methods->reset_connection)(pdbh); + } else if (pdbh->methods->check_liveness) { + alive = (pdbh->methods->check_liveness)(pdbh); + } + if (FAILURE == alive) { + /* Removing it from the persistent list runs the + * persistent destructor, which closes and frees the + * connection. Only correct because refcount is 1. */ + zend_hash_del(&EG(persistent_list), hash_key); + pdbh = NULL; + } + } else if (pdbh->methods->check_liveness + && FAILURE == (pdbh->methods->check_liveness)(pdbh)) { + /* Another open handle shares this connection, so we must + * not reset it: that would discard session state (open + * transactions, temporary tables, user variables) the + * other handle relies on. If it has died, drop our + * reference and stop reusing it; it is closed once the + * last handle referencing it is released. */ pdbh->refcount--; zend_list_close(le); pdbh = NULL; diff --git a/ext/pdo_mysql/tests/pdo_mysql_pconnect_reset.phpt b/ext/pdo_mysql/tests/pdo_mysql_pconnect_reset.phpt index b86d85f21f15..57e5fdd12647 100644 --- a/ext/pdo_mysql/tests/pdo_mysql_pconnect_reset.phpt +++ b/ext/pdo_mysql/tests/pdo_mysql_pconnect_reset.phpt @@ -44,8 +44,8 @@ $tmp = $db2->query('SELECT @test_var')->fetchColumn(); if (null !== $tmp) printf("[003] User variable should have been reset, got %s\n", var_export($tmp, true)); -$stmt = $db2->query("SHOW TABLES LIKE 'pdo_reset_tmp'"); -if (false !== $stmt->fetch()) +/* SHOW TABLES never lists temporary tables, so probe by selecting from it */ +if (false !== $db2->query('SELECT COUNT(*) FROM pdo_reset_tmp')) printf("[004] Temporary table should not exist after reset\n"); echo "done!"; diff --git a/ext/pdo_mysql/tests/pdo_mysql_pconnect_shared_no_reset.phpt b/ext/pdo_mysql/tests/pdo_mysql_pconnect_shared_no_reset.phpt new file mode 100644 index 000000000000..be339eae3beb --- /dev/null +++ b/ext/pdo_mysql/tests/pdo_mysql_pconnect_shared_no_reset.phpt @@ -0,0 +1,49 @@ +--TEST-- +PDO MySQL persistent connection shared by two handles is not reset on reuse +--EXTENSIONS-- +pdo_mysql +--SKIPIF-- + +--FILE-- + true]); +$db1->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); + +/* server-side session state on the shared connection */ +$db1->exec('SET @test_var = 42'); +$db1->exec('CREATE TEMPORARY TABLE pdo_shared_tmp (id INT)'); + +/* Opening a second handle for the same DSN joins the same underlying + * connection while $db1 is still using it. That must not reset the session. */ +$db2 = new PDO($dsn, $user, $pass, [PDO::ATTR_PERSISTENT => true]); +$db2->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); + +/* same physical connection */ +$con1 = $db1->query('SELECT CONNECTION_ID()')->fetchColumn(); +$con2 = $db2->query('SELECT CONNECTION_ID()')->fetchColumn(); +if ($con1 != $con2) + printf("[001] Expected both handles to share one connection (%s vs %s)\n", + var_export($con1, true), var_export($con2, true)); + +/* state set before $db2 existed must survive */ +$tmp = $db1->query('SELECT @test_var')->fetchColumn(); +if ($tmp != 42) + printf("[002] User variable should be preserved, got %s\n", var_export($tmp, true)); + +/* SHOW TABLES never lists temporary tables, so probe by selecting from it */ +if (false === $db1->query('SELECT COUNT(*) FROM pdo_shared_tmp')) + printf("[003] Temporary table should still exist\n"); + +echo "done!"; +?> +--EXPECT-- +done! From d6b714bbbc46fe387fb3be473726d0e30cda3c2d Mon Sep 17 00:00:00 2001 From: Eric Norris Date: Fri, 3 Jul 2026 11:56:14 -0400 Subject: [PATCH 4/9] refactor(pdo): move reset_connection to the end of pdo_dbh_methods Adding reset_connection after check_liveness shifts every member after it. An out-of-tree driver that recompiles against the new struct without updating its positional initializer would then map each value onto the wrong member, so PDO would call the wrong function pointers. Appending reset_connection at the end keeps those values aligned, leaving only the new, unset member NULL, which falls back to check_liveness. An un-updated initializer still warns under -Wmissing-field-initializers regardless of placement; that is why the in-tree drivers get an explicit trailing NULL. Placement only affects whether the remaining values stay correctly aligned. --- ext/pdo/php_pdo_driver.h | 2 +- ext/pdo_dblib/dblib_driver.c | 4 ++-- ext/pdo_firebird/firebird_driver.c | 4 ++-- ext/pdo_mysql/mysql_driver.c | 10 +++++----- ext/pdo_odbc/odbc_driver.c | 4 ++-- ext/pdo_pgsql/pgsql_driver.c | 4 ++-- ext/pdo_sqlite/sqlite_driver.c | 4 ++-- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ext/pdo/php_pdo_driver.h b/ext/pdo/php_pdo_driver.h index 3d3c5398eefc..6c556706f8ba 100644 --- a/ext/pdo/php_pdo_driver.h +++ b/ext/pdo/php_pdo_driver.h @@ -316,13 +316,13 @@ struct pdo_dbh_methods { pdo_dbh_fetch_error_func fetch_err; pdo_dbh_get_attr_func get_attribute; pdo_dbh_check_liveness_func check_liveness; - pdo_dbh_reset_connection_func reset_connection; pdo_dbh_get_driver_methods_func get_driver_methods; pdo_dbh_request_shutdown persistent_shutdown; /* if defined to NULL, PDO will use its internal transaction tracking state */ pdo_dbh_txn_func in_transaction; pdo_dbh_get_gc_func get_gc; pdo_dbh_sql_scanner scanner; + pdo_dbh_reset_connection_func reset_connection; }; /* }}} */ diff --git a/ext/pdo_dblib/dblib_driver.c b/ext/pdo_dblib/dblib_driver.c index 01c50e44217b..96e093311eef 100644 --- a/ext/pdo_dblib/dblib_driver.c +++ b/ext/pdo_dblib/dblib_driver.c @@ -448,12 +448,12 @@ static const struct pdo_dbh_methods dblib_methods = { dblib_fetch_error, /* fetch error */ dblib_get_attribute, /* get attr */ dblib_handle_check_liveness, /* check_liveness */ - NULL, /* reset_connection */ NULL, /* get driver methods */ NULL, /* request shutdown */ NULL, /* in transaction, use PDO's internal tracking mechanism */ NULL, /* get gc */ - NULL /* scanner */ + NULL, /* scanner */ + NULL, /* reset_connection */ }; static int pdo_dblib_handle_factory(pdo_dbh_t *dbh, zval *driver_options) diff --git a/ext/pdo_firebird/firebird_driver.c b/ext/pdo_firebird/firebird_driver.c index 8fb52fb3203b..57ae333dd223 100644 --- a/ext/pdo_firebird/firebird_driver.c +++ b/ext/pdo_firebird/firebird_driver.c @@ -1335,12 +1335,12 @@ static const struct pdo_dbh_methods firebird_methods = { /* {{{ */ pdo_firebird_fetch_error_func, pdo_firebird_get_attribute, pdo_firebird_check_liveness, - NULL, /* reset_connection */ NULL, /* get driver methods */ NULL, /* request shutdown */ pdo_firebird_in_manually_transaction, NULL, /* get gc */ - NULL /* scanner */ + NULL, /* scanner */ + NULL, /* reset_connection */ }; /* }}} */ diff --git a/ext/pdo_mysql/mysql_driver.c b/ext/pdo_mysql/mysql_driver.c index 7acc6ec821a2..8904efca59b7 100644 --- a/ext/pdo_mysql/mysql_driver.c +++ b/ext/pdo_mysql/mysql_driver.c @@ -675,16 +675,16 @@ static const struct pdo_dbh_methods mysql_methods = { pdo_mysql_fetch_error_func, pdo_mysql_get_attribute, pdo_mysql_check_liveness, + NULL, + pdo_mysql_request_shutdown, + pdo_mysql_in_transaction, + NULL, /* get_gc */ + pdo_mysql_scanner, #ifdef PDO_USE_MYSQLND pdo_mysql_reset_connection, #else NULL, /* reset_connection */ #endif - NULL, - pdo_mysql_request_shutdown, - pdo_mysql_in_transaction, - NULL, /* get_gc */ - pdo_mysql_scanner }; /* }}} */ diff --git a/ext/pdo_odbc/odbc_driver.c b/ext/pdo_odbc/odbc_driver.c index 9408dadb7d16..84d10da7129b 100644 --- a/ext/pdo_odbc/odbc_driver.c +++ b/ext/pdo_odbc/odbc_driver.c @@ -461,12 +461,12 @@ static const struct pdo_dbh_methods odbc_methods = { pdo_odbc_fetch_error_func, odbc_handle_get_attr, /* get attr */ odbc_handle_check_liveness, /* check_liveness */ - NULL, /* reset_connection */ NULL, /* get_driver_methods */ NULL, /* request_shutdown */ NULL, /* in transaction, use PDO's internal tracking mechanism */ NULL, /* get_gc */ - NULL /* scanner */ + NULL, /* scanner */ + NULL, /* reset_connection */ }; static int pdo_odbc_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{ */ diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c index 066484d21b02..fc47f8730d64 100644 --- a/ext/pdo_pgsql/pgsql_driver.c +++ b/ext/pdo_pgsql/pgsql_driver.c @@ -1396,12 +1396,12 @@ static const struct pdo_dbh_methods pgsql_methods = { pdo_pgsql_fetch_error_func, pdo_pgsql_get_attribute, pdo_pgsql_check_liveness, /* check_liveness */ - NULL, /* reset_connection */ pdo_pgsql_get_driver_methods, /* get_driver_methods */ pdo_pgsql_request_shutdown, pgsql_handle_in_transaction, NULL, /* get_gc */ - pdo_pgsql_scanner + pdo_pgsql_scanner, + NULL, /* reset_connection */ }; static int pdo_pgsql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{ */ diff --git a/ext/pdo_sqlite/sqlite_driver.c b/ext/pdo_sqlite/sqlite_driver.c index 8f6df4185dd2..1d2b03a03fac 100644 --- a/ext/pdo_sqlite/sqlite_driver.c +++ b/ext/pdo_sqlite/sqlite_driver.c @@ -783,12 +783,12 @@ static const struct pdo_dbh_methods sqlite_methods = { pdo_sqlite_fetch_error_func, pdo_sqlite_get_attribute, NULL, /* check_liveness: not needed */ - NULL, /* reset_connection */ get_driver_methods, pdo_sqlite_request_shutdown, pdo_sqlite_in_transaction, pdo_sqlite_get_gc, - pdo_sqlite_scanner + pdo_sqlite_scanner, + NULL, /* reset_connection */ }; static char *make_filename_safe(const char *filename) From 1a547f36b808d4fb1677e4ab7c5f32c46a19fa3c Mon Sep 17 00:00:00 2001 From: Eric Norris Date: Sun, 2 Aug 2026 11:58:48 -0400 Subject: [PATCH 5/9] feat(mysqlnd): re-initialize connection state after COM_RESET_CONNECTION Before this commit, persistent connections had their character set and the state of any init commmands reset to the connection defaults upon reuse. Furthermore, since since mysqlnd tries to track the character set, mysqlnd would think it was using the desired character set when it in fact was not. After this commit, restart_psession now re-applies the setup a fresh connection establishes. It sets the character set when it differs from the one the server advertised in its greeting (conn->greet_charset) to handle when the user selected a non-default character set via the DSN (charset=), via mysqli_options(MYSQLI_SET_CHARSET_NAME), or via set_charset(). It also executes any configured init commands. --- ext/mysqli/tests/mysqli_pconnect_charset.phpt | 46 +++++++++++++++++++ ext/mysqlnd/mysqlnd_connection.c | 12 +++++ .../tests/pdo_mysql_pconnect_charset.phpt | 42 +++++++++++++++++ .../pdo_mysql_pconnect_init_command.phpt | 44 ++++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 ext/mysqli/tests/mysqli_pconnect_charset.phpt create mode 100644 ext/pdo_mysql/tests/pdo_mysql_pconnect_charset.phpt create mode 100644 ext/pdo_mysql/tests/pdo_mysql_pconnect_init_command.phpt diff --git a/ext/mysqli/tests/mysqli_pconnect_charset.phpt b/ext/mysqli/tests/mysqli_pconnect_charset.phpt new file mode 100644 index 000000000000..90960f5cdf4a --- /dev/null +++ b/ext/mysqli/tests/mysqli_pconnect_charset.phpt @@ -0,0 +1,46 @@ +--TEST-- +mysqli persistent connection restores set_charset() after COM_RESET_CONNECTION reuse +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- + +--EXPECT-- +done! diff --git a/ext/mysqlnd/mysqlnd_connection.c b/ext/mysqlnd/mysqlnd_connection.c index b17c753f2ce6..49af67ca5f02 100644 --- a/ext/mysqlnd/mysqlnd_connection.c +++ b/ext/mysqlnd/mysqlnd_connection.c @@ -364,6 +364,18 @@ MYSQLND_METHOD(mysqlnd_conn_data, restart_psession)(MYSQLND_CONN_DATA * conn) MYSQLND_INC_CONN_STATISTIC(conn->stats, STAT_CONNECT_REUSED); conn->current_result = NULL; conn->last_message.s = NULL; + + /* COM_RESET_CONNECTION reverts the session charset to the server + * default, but the user may have changed the charset. Re-apply + * conn->charset when it differs from that default. */ + if (conn->charset && conn->charset != conn->greet_charset) { + ret = conn->m->set_charset(conn, conn->charset->name); + } + + /* Re-execute any MYSQL_INIT_COMMAND commands. */ + if (ret == PASS) { + ret = conn->m->execute_init_commands(conn); + } } DBG_RETURN(ret); diff --git a/ext/pdo_mysql/tests/pdo_mysql_pconnect_charset.phpt b/ext/pdo_mysql/tests/pdo_mysql_pconnect_charset.phpt new file mode 100644 index 000000000000..e7527a44aace --- /dev/null +++ b/ext/pdo_mysql/tests/pdo_mysql_pconnect_charset.phpt @@ -0,0 +1,42 @@ +--TEST-- +PDO MySQL persistent connection restores the DSN charset after COM_RESET_CONNECTION reuse +--EXTENSIONS-- +pdo_mysql +--SKIPIF-- + +--FILE-- + true]); +$db1->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); +$con1 = $db1->query('SELECT CONNECTION_ID()')->fetchColumn(); +$cs1 = $db1->query('SELECT @@session.character_set_connection')->fetchColumn(); +if ($cs1 !== 'latin1') + printf("[001] Expected latin1, got %s\n", var_export($cs1, true)); + +$db1 = null; /* return to the pool */ + +/* reuse fires COM_RESET_CONNECTION, which reverts the charset; it must be re-applied */ +$db2 = new PDO($dsn, $user, $pass, [PDO::ATTR_PERSISTENT => true]); +$db2->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); +$con2 = $db2->query('SELECT CONNECTION_ID()')->fetchColumn(); +if ($con1 != $con2) + printf("[002] Expected the pooled connection to be reused (%s vs %s)\n", + var_export($con1, true), var_export($con2, true)); + +$cs2 = $db2->query('SELECT @@session.character_set_connection')->fetchColumn(); +if ($cs2 !== 'latin1') + printf("[003] Charset should be restored to latin1 after reuse, got %s\n", var_export($cs2, true)); + +echo "done!"; +?> +--EXPECT-- +done! diff --git a/ext/pdo_mysql/tests/pdo_mysql_pconnect_init_command.phpt b/ext/pdo_mysql/tests/pdo_mysql_pconnect_init_command.phpt new file mode 100644 index 000000000000..797316b59e30 --- /dev/null +++ b/ext/pdo_mysql/tests/pdo_mysql_pconnect_init_command.phpt @@ -0,0 +1,44 @@ +--TEST-- +PDO MySQL persistent connection re-runs INIT_COMMAND after COM_RESET_CONNECTION reuse +--EXTENSIONS-- +pdo_mysql +--SKIPIF-- + +--FILE-- + true, + Pdo\Mysql::ATTR_INIT_COMMAND => 'SET @init_marker = 42', +]; + +$db1 = new PDO($dsn, $user, $pass, $opts); +$db1->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); +$con1 = $db1->query('SELECT CONNECTION_ID()')->fetchColumn(); +if ($db1->query('SELECT @init_marker')->fetchColumn() != 42) + printf("[001] INIT_COMMAND should have set @init_marker\n"); + +$db1 = null; /* return to the pool */ + +/* reuse fires COM_RESET_CONNECTION, which clears @init_marker; INIT_COMMAND must be re-run */ +$db2 = new PDO($dsn, $user, $pass, $opts); +$db2->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); +$con2 = $db2->query('SELECT CONNECTION_ID()')->fetchColumn(); +if ($con1 != $con2) + printf("[002] Expected the pooled connection to be reused\n"); + +if ($db2->query('SELECT @init_marker')->fetchColumn() != 42) + printf("[003] INIT_COMMAND should be re-run after reuse (got %s)\n", + var_export($db2->query('SELECT @init_marker')->fetchColumn(), true)); + +echo "done!"; +?> +--EXPECT-- +done! From c8f739547fca6f5f4a7f538bdad750d925e25db3 Mon Sep 17 00:00:00 2001 From: Eric Norris Date: Sun, 2 Aug 2026 11:59:01 -0400 Subject: [PATCH 6/9] refactor(pdo): address @TimWolla's comment This commit reorganizes the persistent-reuse refcount check to make the `else` more clearly a block where `refcount > 1`, so the purpose of the next check (`if (pdbh->methods->check_liveness ...`) is also more clear. --- ext/pdo/pdo_dbh.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ext/pdo/pdo_dbh.c b/ext/pdo/pdo_dbh.c index dc046c347a08..886217e98d95 100644 --- a/ext/pdo/pdo_dbh.c +++ b/ext/pdo/pdo_dbh.c @@ -433,17 +433,19 @@ PDO_API void php_pdo_internal_construct_driver(INTERNAL_FUNCTION_PARAMETERS, zen zend_hash_del(&EG(persistent_list), hash_key); pdbh = NULL; } - } else if (pdbh->methods->check_liveness - && FAILURE == (pdbh->methods->check_liveness)(pdbh)) { + } else { /* Another open handle shares this connection, so we must * not reset it: that would discard session state (open * transactions, temporary tables, user variables) the * other handle relies on. If it has died, drop our * reference and stop reusing it; it is closed once the * last handle referencing it is released. */ - pdbh->refcount--; - zend_list_close(le); - pdbh = NULL; + if (pdbh->methods->check_liveness + && FAILURE == (pdbh->methods->check_liveness)(pdbh)) { + pdbh->refcount--; + zend_list_close(le); + pdbh = NULL; + } } } } From cf5ef9aa127129414a7ff428c763635239d72d6e Mon Sep 17 00:00:00 2001 From: Eric Norris Date: Tue, 4 Aug 2026 10:52:34 -0400 Subject: [PATCH 7/9] feat(mysqlnd): only restore the connection-option charset after reset Before this commit, we were restoring the current charset in a persistent connection regardless of when it was set - for example, we would restore a charset established by `set_charset()` at any point in the request. @TimWolla made a good point that we should instead only preserve charsets configured at *connection time*. After this commit, we only restore a charset configured in the DSN. Any post-connection charset configuration is reset to the server's default. --- ext/mysqli/tests/mysqli_pconnect_charset.phpt | 27 ++++++++++--------- ext/mysqlnd/mysqlnd_connection.c | 14 +++++++--- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/ext/mysqli/tests/mysqli_pconnect_charset.phpt b/ext/mysqli/tests/mysqli_pconnect_charset.phpt index 90960f5cdf4a..6e3f6fb59658 100644 --- a/ext/mysqli/tests/mysqli_pconnect_charset.phpt +++ b/ext/mysqli/tests/mysqli_pconnect_charset.phpt @@ -1,5 +1,5 @@ --TEST-- -mysqli persistent connection restores set_charset() after COM_RESET_CONNECTION reuse +mysqli persistent connection does not restore a post-connect set_charset() after reuse --EXTENSIONS-- mysqli --SKIPIF-- @@ -16,27 +16,30 @@ $link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket); if (!$link) printf("[001] Cannot connect\n"); +/* the charset the connection starts with; no charset was requested for it */ +$default = mysqli_character_set_name($link); +if ($default === 'latin1') + printf("[002] Test needs a default charset other than latin1, got %s\n", $default); + +/* change it after the connection has been established */ mysqli_set_charset($link, 'latin1'); -if (mysqli_character_set_name($link) !== 'latin1') - printf("[002] Expected latin1, got %s\n", mysqli_character_set_name($link)); $thread_id = mysqli_thread_id($link); mysqli_close($link); -/* reuse fires COM_RESET_CONNECTION, which reverts the charset on the server */ +/* reuse fires COM_RESET_CONNECTION */ $link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket); if (mysqli_thread_id($link) !== $thread_id) printf("[003] Expected the pooled connection to be reused\n"); +/* set_charset() runs after connect, so the reset does not restore it */ +if (mysqli_character_set_name($link) !== $default) + printf("[004] Charset should be reset to %s, got %s\n", $default, mysqli_character_set_name($link)); + /* mysqlnd's cached charset (used for escaping) must still agree with the server */ -if (mysqli_character_set_name($link) !== 'latin1') - printf("[004] Client-side charset should still be latin1, got %s\n", mysqli_character_set_name($link)); - -$res = mysqli_query($link, "SELECT @@session.character_set_connection AS c"); -$row = mysqli_fetch_assoc($res); -if ($row['c'] !== 'latin1') - printf("[005] Server charset should be restored to latin1, got %s\n", $row['c']); -mysqli_free_result($res); +$server = mysqli_query($link, "SELECT @@session.character_set_connection")->fetch_row()[0]; +if (mysqli_character_set_name($link) !== $server) + printf("[005] Client charset %s disagrees with server %s\n", mysqli_character_set_name($link), $server); mysqli_close($link); diff --git a/ext/mysqlnd/mysqlnd_connection.c b/ext/mysqlnd/mysqlnd_connection.c index 49af67ca5f02..1d4119477de2 100644 --- a/ext/mysqlnd/mysqlnd_connection.c +++ b/ext/mysqlnd/mysqlnd_connection.c @@ -366,10 +366,16 @@ MYSQLND_METHOD(mysqlnd_conn_data, restart_psession)(MYSQLND_CONN_DATA * conn) conn->last_message.s = NULL; /* COM_RESET_CONNECTION reverts the session charset to the server - * default, but the user may have changed the charset. Re-apply - * conn->charset when it differs from that default. */ - if (conn->charset && conn->charset != conn->greet_charset) { - ret = conn->m->set_charset(conn, conn->charset->name); + * default. A charset requested as a connection option (DSN charset=, + * MYSQLI_SET_CHARSET_NAME) is part of establishing the connection, so + * re-apply it to match a fresh connect. A charset chosen afterwards + * with set_charset() is not restored - the caller reapplies that + * itself - but realign the cached charset (used for escaping) with the + * now-reset connection. */ + if (conn->options->charset_name) { + ret = conn->m->set_charset(conn, conn->options->charset_name); + } else if (conn->greet_charset) { + conn->charset = conn->greet_charset; } /* Re-execute any MYSQL_INIT_COMMAND commands. */ From b3a44806931c4b035297e338198f424ddcb72921 Mon Sep 17 00:00:00 2001 From: Eric Norris Date: Tue, 4 Aug 2026 11:01:06 -0400 Subject: [PATCH 8/9] refactor(pdo): address review comments Besides the minor diff to `pdo_dbh.c`, in this commit I've refactored all of the tests to ensure they cover the cases described in the review comments and are clear about what they are doing. --- ext/mysqli/tests/mysqli_pconn_preserved.phpt | 88 +++++++++++++++++ ext/mysqli/tests/mysqli_pconn_reset.phpt | 95 +++++++++++++++++++ ext/mysqli/tests/mysqli_pconnect_charset.phpt | 49 ---------- ext/mysqli/tests/mysqli_pconnect_reset.phpt | 60 ------------ ext/pdo/pdo_dbh.c | 3 +- .../tests/pdo_mysql_pconnect_charset.phpt | 42 -------- .../pdo_mysql_pconnect_init_command.phpt | 44 --------- .../tests/pdo_mysql_pconnect_preserved.phpt | 76 +++++++++++++++ .../tests/pdo_mysql_pconnect_reset.phpt | 61 ++++++++---- .../tests/pdo_mysql_pconnect_shared.phpt | 56 +++++++++++ .../pdo_mysql_pconnect_shared_no_reset.phpt | 49 ---------- 11 files changed, 356 insertions(+), 267 deletions(-) create mode 100644 ext/mysqli/tests/mysqli_pconn_preserved.phpt create mode 100644 ext/mysqli/tests/mysqli_pconn_reset.phpt delete mode 100644 ext/mysqli/tests/mysqli_pconnect_charset.phpt delete mode 100644 ext/mysqli/tests/mysqli_pconnect_reset.phpt delete mode 100644 ext/pdo_mysql/tests/pdo_mysql_pconnect_charset.phpt delete mode 100644 ext/pdo_mysql/tests/pdo_mysql_pconnect_init_command.phpt create mode 100644 ext/pdo_mysql/tests/pdo_mysql_pconnect_preserved.phpt create mode 100644 ext/pdo_mysql/tests/pdo_mysql_pconnect_shared.phpt delete mode 100644 ext/pdo_mysql/tests/pdo_mysql_pconnect_shared_no_reset.phpt diff --git a/ext/mysqli/tests/mysqli_pconn_preserved.phpt b/ext/mysqli/tests/mysqli_pconn_preserved.phpt new file mode 100644 index 000000000000..87bfda6941df --- /dev/null +++ b/ext/mysqli/tests/mysqli_pconn_preserved.phpt @@ -0,0 +1,88 @@ +--TEST-- +mysqli: reusing a persistent connection re-applies its connection config after reset +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +fetch_row()[0]; + +if ($marker != 42) { + printf("[003] Init command should have set @init_marker at connect, got %s\n", var_export($marker, true)); +} + +// Return the connection to the pool. +mysqli_close($link); + +// Reopen the same persistent connection, which should be reset, but preserve +// the options from the initial configuration. +$link = connect_configured($host, $user, $passwd, $db, $port, $socket); + +// Compare the thread IDs to ensure the connection was reused. +if (mysqli_thread_id($link) !== $thread_id) { + printf("[004] Expected the pooled connection to be reused\n"); +} + +// Check the charset to ensure the configured one is back in effect. +if (mysqli_character_set_name($link) !== 'latin1') { + printf("[005] Charset should be re-applied to latin1, got %s\n", mysqli_character_set_name($link)); +} + +// Check the server-side charset to ensure it agrees with the client. +$server = mysqli_query($link, "SELECT @@session.character_set_connection")->fetch_row()[0]; + +if ($server !== 'latin1') { + printf("[006] Server charset should be latin1, got %s\n", $server); +} + +// Check the session variable to ensure the init command was re-executed. +$marker = mysqli_query($link, "SELECT @init_marker")->fetch_row()[0]; + +if ($marker != 42) { + printf("[007] Init command should be re-run after reuse, got %s\n", var_export($marker, true)); +} + +mysqli_close($link); + +echo "done!"; +?> +--EXPECT-- +done! diff --git a/ext/mysqli/tests/mysqli_pconn_reset.phpt b/ext/mysqli/tests/mysqli_pconn_reset.phpt new file mode 100644 index 000000000000..7dd7aba5d49e --- /dev/null +++ b/ext/mysqli/tests/mysqli_pconn_reset.phpt @@ -0,0 +1,95 @@ +--TEST-- +mysqli: reusing a persistent connection resets its session state (COM_RESET_CONNECTION) +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +fetch_row()[0]; + +if ($v !== null) { + printf("[004] User variable should be reset, got %s\n", var_export($v, true)); +} + +// Check the temporary table to ensure it is no longer present. +if (@mysqli_query($link, "SELECT 1 FROM test_reset_tmp") !== false) { + printf("[005] Temporary table should not exist after reset\n"); +} + +// Check to see we are no longer in a transaction. +$rows = mysqli_query($link, "SELECT COUNT(*) FROM test_reset_trx")->fetch_row()[0]; + +if ($rows != 0) { + printf("[006] Transaction should have been rolled back, found %d row(s)\n", $rows); +} + +// A charset set after connecting is not part of the config, so it is not restored. +if (mysqli_character_set_name($link) !== $default) { + printf("[007] Charset should be reset to %s, got %s\n", $default, mysqli_character_set_name($link)); +} + +// mysqlnd's cached charset (used for escaping) must still agree with the server. +$server = mysqli_query($link, "SELECT @@session.character_set_connection")->fetch_row()[0]; + +if (mysqli_character_set_name($link) !== $server) { + printf("[008] Client charset %s disagrees with server %s\n", mysqli_character_set_name($link), $server); +} + +// Clean up the non-temporary table. +mysqli_query($link, "DROP TABLE IF EXISTS test_reset_trx"); +mysqli_close($link); + +echo "done!"; +?> +--EXPECT-- +done! diff --git a/ext/mysqli/tests/mysqli_pconnect_charset.phpt b/ext/mysqli/tests/mysqli_pconnect_charset.phpt deleted file mode 100644 index 6e3f6fb59658..000000000000 --- a/ext/mysqli/tests/mysqli_pconnect_charset.phpt +++ /dev/null @@ -1,49 +0,0 @@ ---TEST-- -mysqli persistent connection does not restore a post-connect set_charset() after reuse ---EXTENSIONS-- -mysqli ---SKIPIF-- - ---FILE-- -fetch_row()[0]; -if (mysqli_character_set_name($link) !== $server) - printf("[005] Client charset %s disagrees with server %s\n", mysqli_character_set_name($link), $server); - -mysqli_close($link); - -echo "done!"; -?> ---EXPECT-- -done! diff --git a/ext/mysqli/tests/mysqli_pconnect_reset.phpt b/ext/mysqli/tests/mysqli_pconnect_reset.phpt deleted file mode 100644 index c5cc7fbb1cb2..000000000000 --- a/ext/mysqli/tests/mysqli_pconnect_reset.phpt +++ /dev/null @@ -1,60 +0,0 @@ ---TEST-- -mysqli_pconnect() - COM_RESET_CONNECTION clears session state ---EXTENSIONS-- -mysqli ---SKIPIF-- - ---FILE-- - ---EXPECT-- -done! diff --git a/ext/pdo/pdo_dbh.c b/ext/pdo/pdo_dbh.c index 886217e98d95..c17090549ed0 100644 --- a/ext/pdo/pdo_dbh.c +++ b/ext/pdo/pdo_dbh.c @@ -440,8 +440,7 @@ PDO_API void php_pdo_internal_construct_driver(INTERNAL_FUNCTION_PARAMETERS, zen * other handle relies on. If it has died, drop our * reference and stop reusing it; it is closed once the * last handle referencing it is released. */ - if (pdbh->methods->check_liveness - && FAILURE == (pdbh->methods->check_liveness)(pdbh)) { + if (pdbh->methods->check_liveness && FAILURE == (pdbh->methods->check_liveness)(pdbh)) { pdbh->refcount--; zend_list_close(le); pdbh = NULL; diff --git a/ext/pdo_mysql/tests/pdo_mysql_pconnect_charset.phpt b/ext/pdo_mysql/tests/pdo_mysql_pconnect_charset.phpt deleted file mode 100644 index e7527a44aace..000000000000 --- a/ext/pdo_mysql/tests/pdo_mysql_pconnect_charset.phpt +++ /dev/null @@ -1,42 +0,0 @@ ---TEST-- -PDO MySQL persistent connection restores the DSN charset after COM_RESET_CONNECTION reuse ---EXTENSIONS-- -pdo_mysql ---SKIPIF-- - ---FILE-- - true]); -$db1->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); -$con1 = $db1->query('SELECT CONNECTION_ID()')->fetchColumn(); -$cs1 = $db1->query('SELECT @@session.character_set_connection')->fetchColumn(); -if ($cs1 !== 'latin1') - printf("[001] Expected latin1, got %s\n", var_export($cs1, true)); - -$db1 = null; /* return to the pool */ - -/* reuse fires COM_RESET_CONNECTION, which reverts the charset; it must be re-applied */ -$db2 = new PDO($dsn, $user, $pass, [PDO::ATTR_PERSISTENT => true]); -$db2->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); -$con2 = $db2->query('SELECT CONNECTION_ID()')->fetchColumn(); -if ($con1 != $con2) - printf("[002] Expected the pooled connection to be reused (%s vs %s)\n", - var_export($con1, true), var_export($con2, true)); - -$cs2 = $db2->query('SELECT @@session.character_set_connection')->fetchColumn(); -if ($cs2 !== 'latin1') - printf("[003] Charset should be restored to latin1 after reuse, got %s\n", var_export($cs2, true)); - -echo "done!"; -?> ---EXPECT-- -done! diff --git a/ext/pdo_mysql/tests/pdo_mysql_pconnect_init_command.phpt b/ext/pdo_mysql/tests/pdo_mysql_pconnect_init_command.phpt deleted file mode 100644 index 797316b59e30..000000000000 --- a/ext/pdo_mysql/tests/pdo_mysql_pconnect_init_command.phpt +++ /dev/null @@ -1,44 +0,0 @@ ---TEST-- -PDO MySQL persistent connection re-runs INIT_COMMAND after COM_RESET_CONNECTION reuse ---EXTENSIONS-- -pdo_mysql ---SKIPIF-- - ---FILE-- - true, - Pdo\Mysql::ATTR_INIT_COMMAND => 'SET @init_marker = 42', -]; - -$db1 = new PDO($dsn, $user, $pass, $opts); -$db1->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); -$con1 = $db1->query('SELECT CONNECTION_ID()')->fetchColumn(); -if ($db1->query('SELECT @init_marker')->fetchColumn() != 42) - printf("[001] INIT_COMMAND should have set @init_marker\n"); - -$db1 = null; /* return to the pool */ - -/* reuse fires COM_RESET_CONNECTION, which clears @init_marker; INIT_COMMAND must be re-run */ -$db2 = new PDO($dsn, $user, $pass, $opts); -$db2->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); -$con2 = $db2->query('SELECT CONNECTION_ID()')->fetchColumn(); -if ($con1 != $con2) - printf("[002] Expected the pooled connection to be reused\n"); - -if ($db2->query('SELECT @init_marker')->fetchColumn() != 42) - printf("[003] INIT_COMMAND should be re-run after reuse (got %s)\n", - var_export($db2->query('SELECT @init_marker')->fetchColumn(), true)); - -echo "done!"; -?> ---EXPECT-- -done! diff --git a/ext/pdo_mysql/tests/pdo_mysql_pconnect_preserved.phpt b/ext/pdo_mysql/tests/pdo_mysql_pconnect_preserved.phpt new file mode 100644 index 000000000000..a08c4e128887 --- /dev/null +++ b/ext/pdo_mysql/tests/pdo_mysql_pconnect_preserved.phpt @@ -0,0 +1,76 @@ +--TEST-- +PDO MySQL: reusing a persistent connection re-applies its connection config after reset +--EXTENSIONS-- +pdo_mysql +--SKIPIF-- + +--FILE-- + true, + PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT, + Pdo\Mysql::ATTR_INIT_COMMAND => 'SET @init_marker = 42', +]; + + +// Establish a persistent handle with the above options. +$db1 = new PDO($dsn, $user, $pass, $opts); +$con1 = $db1->query('SELECT CONNECTION_ID()')->fetchColumn(); + +// Sanity check that the charset was configured. +$charset = $db1->query('SELECT @@session.character_set_connection')->fetchColumn(); + +if ($charset !== 'latin1') { + printf("[001] Charset should be latin1 at connect, got %s\n", var_export($charset, true)); +} + +// Sanity check that the ATTR_INIT_COMMAND was executed. +$marker = $db1->query('SELECT @init_marker')->fetchColumn(); + +if ($marker != 42) { + printf("[002] Init command should have set @init_marker at connect, got %s\n", var_export($marker, true)); +} + +// Return the connection to the pool. +$db1 = null; + +// Open a second handle for the same DSN, which should be reset, but preserve +// the options from the initial configuration. +$db2 = new PDO($dsn, $user, $pass, $opts); +$con2 = $db2->query('SELECT CONNECTION_ID()')->fetchColumn(); + +// Compare the connection IDs to ensure they are the same. +if ($con1 != $con2) { + printf("[003] Expected the pooled connection to be reused (%s vs %s)\n", + var_export($con1, true), var_export($con2, true)); +} + +// Check the charset to ensure we're using the one configured in the DSN. +$charset = $db2->query('SELECT @@session.character_set_connection')->fetchColumn(); + +if ($charset !== 'latin1') { + printf("[004] Charset should be re-applied to latin1, got %s\n", var_export($charset, true)); +} + +// Check the session variable to ensure the ATTR_INIT_COMMAND was re-executed. +$marker = $db2->query('SELECT @init_marker')->fetchColumn(); + +if ($marker != 42) { + printf("[005] Init command should be re-run after reuse, got %s\n", var_export($marker, true)); +} + +echo "done!"; +?> +--EXPECT-- +done! diff --git a/ext/pdo_mysql/tests/pdo_mysql_pconnect_reset.phpt b/ext/pdo_mysql/tests/pdo_mysql_pconnect_reset.phpt index 57e5fdd12647..bd37a90c58a6 100644 --- a/ext/pdo_mysql/tests/pdo_mysql_pconnect_reset.phpt +++ b/ext/pdo_mysql/tests/pdo_mysql_pconnect_reset.phpt @@ -1,5 +1,5 @@ --TEST-- -PDO MySQL persistent connection session state is reset (COM_RESET_CONNECTION) on reuse +PDO MySQL: reusing a persistent connection resets its session state (COM_RESET_CONNECTION) --EXTENSIONS-- pdo_mysql --SKIPIF-- @@ -9,44 +9,63 @@ MySQLPDOTest::skip(); ?> --FILE-- true, PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT]; -$db1 = new PDO($dsn, $user, $pass, [PDO::ATTR_PERSISTENT => true]); -$db1->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); - +// Establish a persistent handle with some session state. +$db1 = new PDO($dsn, $user, $pass, $opts); $con1 = $db1->query('SELECT CONNECTION_ID()')->fetchColumn(); -/* leave some server-side session state behind */ $db1->exec('SET @test_var = 42'); $db1->exec('CREATE TEMPORARY TABLE pdo_reset_tmp (id INT)'); -$tmp = $db1->query('SELECT @test_var')->fetchColumn(); -if ($tmp != 42) - printf("[001] Expected 42, got %s\n", var_export($tmp, true)); +$db1->exec('DROP TABLE IF EXISTS pdo_reset_trx'); +$db1->exec('CREATE TABLE pdo_reset_trx (id INT) ENGINE=InnoDB'); -/* release the handle; the persistent connection stays pooled */ -$db1 = null; +$db1->beginTransaction(); +$db1->exec('INSERT INTO pdo_reset_trx VALUES (1)'); -/* reusing the pooled connection must reset its session state */ -$db2 = new PDO($dsn, $user, $pass, [PDO::ATTR_PERSISTENT => true]); -$db2->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); +// Return the connection to the pool. +$db1 = null; +// Open a second handle for the same DSN, which should be reset. +$db2 = new PDO($dsn, $user, $pass, $opts); $con2 = $db2->query('SELECT CONNECTION_ID()')->fetchColumn(); -if ($con1 != $con2) - printf("[002] Expected the pooled connection to be reused (%s vs %s)\n", - var_export($con1, true), var_export($con2, true)); +// Compare the connection IDs to ensure they are different. +if ($con1 != $con2) { + printf("[001] Expected the pooled connection to be reused (%s vs %s)\n", + var_export($con1, true), var_export($con2, true)); +} + +// Check the session variable to ensure it is no longer present. $tmp = $db2->query('SELECT @test_var')->fetchColumn(); -if (null !== $tmp) - printf("[003] User variable should have been reset, got %s\n", var_export($tmp, true)); -/* SHOW TABLES never lists temporary tables, so probe by selecting from it */ -if (false !== $db2->query('SELECT COUNT(*) FROM pdo_reset_tmp')) - printf("[004] Temporary table should not exist after reset\n"); +if ($tmp !== null) { + printf("[002] User variable should be reset, got %s\n", var_export($tmp, true)); +} + +// Check the temporary table to ensure it is no longer present. +if ($db2->query('SELECT COUNT(*) FROM pdo_reset_tmp') !== false) { + printf("[003] Temporary table should not exist after reset\n"); +} + +// Check to see we are no longer in a transaction. +$rows = $db2->query('SELECT COUNT(*) FROM pdo_reset_trx')->fetchColumn(); + +if ($rows != 0) { + printf("[004] Transaction should have been rolled back, found %s row(s)\n", var_export($rows, true)); +} + +// Clean up the non-temporary table. +$db2->exec('DROP TABLE IF EXISTS pdo_reset_trx'); echo "done!"; ?> diff --git a/ext/pdo_mysql/tests/pdo_mysql_pconnect_shared.phpt b/ext/pdo_mysql/tests/pdo_mysql_pconnect_shared.phpt new file mode 100644 index 000000000000..65404c0c53b4 --- /dev/null +++ b/ext/pdo_mysql/tests/pdo_mysql_pconnect_shared.phpt @@ -0,0 +1,56 @@ +--TEST-- +PDO MySQL: a persistent connection shared by two live handles is not reset +--EXTENSIONS-- +pdo_mysql +--SKIPIF-- + +--FILE-- + true, PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT]; + +// Establish a persistent handle with some session state. +$db1 = new PDO($dsn, $user, $pass, $opts); +$db1->exec('SET @test_var = 42'); +$db1->exec('CREATE TEMPORARY TABLE pdo_shared_tmp (id INT)'); + +// Open a second handle for the same DSN while $db1 is still alive. +$db2 = new PDO($dsn, $user, $pass, $opts); + +// Compare the connection IDs to ensure they are the same. +$con1 = $db1->query('SELECT CONNECTION_ID()')->fetchColumn(); +$con2 = $db2->query('SELECT CONNECTION_ID()')->fetchColumn(); + +if ($con1 != $con2) { + printf("[001] Expected both handles to share one connection (%s vs %s)\n", + var_export($con1, true), var_export($con2, true)); +} + +// Check the session variable to ensure it is the same. +$tmp = $db1->query('SELECT @test_var')->fetchColumn(); + +if ($tmp != 42) { + printf("[002] User variable should be preserved, got %s\n", var_export($tmp, true)); +} + +// Ensure the temporary table still exists. +if (false === $db1->query('SELECT COUNT(*) FROM pdo_shared_tmp')) { + printf("[003] Temporary table should still exist\n"); +} + +echo "done!"; +?> +--EXPECT-- +done! diff --git a/ext/pdo_mysql/tests/pdo_mysql_pconnect_shared_no_reset.phpt b/ext/pdo_mysql/tests/pdo_mysql_pconnect_shared_no_reset.phpt deleted file mode 100644 index be339eae3beb..000000000000 --- a/ext/pdo_mysql/tests/pdo_mysql_pconnect_shared_no_reset.phpt +++ /dev/null @@ -1,49 +0,0 @@ ---TEST-- -PDO MySQL persistent connection shared by two handles is not reset on reuse ---EXTENSIONS-- -pdo_mysql ---SKIPIF-- - ---FILE-- - true]); -$db1->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); - -/* server-side session state on the shared connection */ -$db1->exec('SET @test_var = 42'); -$db1->exec('CREATE TEMPORARY TABLE pdo_shared_tmp (id INT)'); - -/* Opening a second handle for the same DSN joins the same underlying - * connection while $db1 is still using it. That must not reset the session. */ -$db2 = new PDO($dsn, $user, $pass, [PDO::ATTR_PERSISTENT => true]); -$db2->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); - -/* same physical connection */ -$con1 = $db1->query('SELECT CONNECTION_ID()')->fetchColumn(); -$con2 = $db2->query('SELECT CONNECTION_ID()')->fetchColumn(); -if ($con1 != $con2) - printf("[001] Expected both handles to share one connection (%s vs %s)\n", - var_export($con1, true), var_export($con2, true)); - -/* state set before $db2 existed must survive */ -$tmp = $db1->query('SELECT @test_var')->fetchColumn(); -if ($tmp != 42) - printf("[002] User variable should be preserved, got %s\n", var_export($tmp, true)); - -/* SHOW TABLES never lists temporary tables, so probe by selecting from it */ -if (false === $db1->query('SELECT COUNT(*) FROM pdo_shared_tmp')) - printf("[003] Temporary table should still exist\n"); - -echo "done!"; -?> ---EXPECT-- -done! From 06cef87f6cbc0a34c6a6c3132a687d87a0316cba Mon Sep 17 00:00:00 2001 From: Eric Norris Date: Tue, 4 Aug 2026 14:16:20 -0400 Subject: [PATCH 9/9] test: check that variable and charset actually reset --- ext/mysqli/tests/mysqli_pconn_preserved.phpt | 5 +++++ ext/pdo_mysql/tests/pdo_mysql_pconnect_preserved.phpt | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/ext/mysqli/tests/mysqli_pconn_preserved.phpt b/ext/mysqli/tests/mysqli_pconn_preserved.phpt index 87bfda6941df..668669d1b2d6 100644 --- a/ext/mysqli/tests/mysqli_pconn_preserved.phpt +++ b/ext/mysqli/tests/mysqli_pconn_preserved.phpt @@ -49,6 +49,11 @@ if ($marker != 42) { printf("[003] Init command should have set @init_marker at connect, got %s\n", var_export($marker, true)); } +// Drift both values away from the configuration, so the checks after reuse prove +// the config was re-applied by the reset rather than merely carried over. +mysqli_set_charset($link, 'ascii'); +mysqli_query($link, "SET @init_marker = 99"); + // Return the connection to the pool. mysqli_close($link); diff --git a/ext/pdo_mysql/tests/pdo_mysql_pconnect_preserved.phpt b/ext/pdo_mysql/tests/pdo_mysql_pconnect_preserved.phpt index a08c4e128887..cb964907a31b 100644 --- a/ext/pdo_mysql/tests/pdo_mysql_pconnect_preserved.phpt +++ b/ext/pdo_mysql/tests/pdo_mysql_pconnect_preserved.phpt @@ -42,6 +42,11 @@ if ($marker != 42) { printf("[002] Init command should have set @init_marker at connect, got %s\n", var_export($marker, true)); } +// Drift both values away from the configuration, so the checks after reuse prove +// the config was re-applied by the reset rather than merely carried over. +$db1->exec('SET NAMES ascii'); +$db1->exec('SET @init_marker = 99'); + // Return the connection to the pool. $db1 = null;