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_pconn_preserved.phpt b/ext/mysqli/tests/mysqli_pconn_preserved.phpt new file mode 100644 index 000000000000..668669d1b2d6 --- /dev/null +++ b/ext/mysqli/tests/mysqli_pconn_preserved.phpt @@ -0,0 +1,93 @@ +--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)); +} + +// 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); + +// 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/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 517b149c0b4d..1d4119477de2 100644 --- a/ext/mysqlnd/mysqlnd_connection.c +++ b/ext/mysqlnd/mysqlnd_connection.c @@ -354,14 +354,37 @@ 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; + + /* COM_RESET_CONNECTION reverts the session charset to the server + * 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. */ + if (ret == PASS) { + ret = conn->m->execute_init_commands(conn); + } + } + + 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/pdo_dbh.c b/ext/pdo/pdo_dbh.c index dcd9f7b126df..a754f9e2b846 100644 --- a/ext/pdo/pdo_dbh.c +++ b/ext/pdo/pdo_dbh.c @@ -414,12 +414,37 @@ 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)) { - /* nope... need to kill it */ - pdbh->refcount--; - zend_list_close(le); - pdbh = NULL; + 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 { + /* 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. */ + if (pdbh->methods->check_liveness && FAILURE == (pdbh->methods->check_liveness)(pdbh)) { + pdbh->refcount--; + zend_list_close(le); + pdbh = NULL; + } } } } diff --git a/ext/pdo/php_pdo_driver.h b/ext/pdo/php_pdo_driver.h index 479a1b5436c9..6c556706f8ba 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 */ @@ -313,6 +322,7 @@ struct pdo_dbh_methods { 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 7189954ec412..96e093311eef 100644 --- a/ext/pdo_dblib/dblib_driver.c +++ b/ext/pdo_dblib/dblib_driver.c @@ -452,7 +452,8 @@ static const struct pdo_dbh_methods dblib_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 36f380b2122b..57ae333dd223 100644 --- a/ext/pdo_firebird/firebird_driver.c +++ b/ext/pdo_firebird/firebird_driver.c @@ -1339,7 +1339,8 @@ static const struct pdo_dbh_methods firebird_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 54a8803971ca..8904efca59b7 100644 --- a/ext/pdo_mysql/mysql_driver.c +++ b/ext/pdo_mysql/mysql_driver.c @@ -610,6 +610,27 @@ static zend_result pdo_mysql_check_liveness(pdo_dbh_t *dbh) } /* }}} */ +#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); + } + PDO_DBG_RETURN(SUCCESS); +} +/* }}} */ +#endif + /* {{{ pdo_mysql_request_shutdown */ static void pdo_mysql_request_shutdown(pdo_dbh_t *dbh) { @@ -658,7 +679,12 @@ static const struct pdo_dbh_methods mysql_methods = { pdo_mysql_request_shutdown, pdo_mysql_in_transaction, NULL, /* get_gc */ - pdo_mysql_scanner + pdo_mysql_scanner, +#ifdef PDO_USE_MYSQLND + pdo_mysql_reset_connection, +#else + NULL, /* reset_connection */ +#endif }; /* }}} */ @@ -724,11 +750,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; 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..cb964907a31b --- /dev/null +++ b/ext/pdo_mysql/tests/pdo_mysql_pconnect_preserved.phpt @@ -0,0 +1,81 @@ +--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)); +} + +// 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; + +// 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 new file mode 100644 index 000000000000..bd37a90c58a6 --- /dev/null +++ b/ext/pdo_mysql/tests/pdo_mysql_pconnect_reset.phpt @@ -0,0 +1,73 @@ +--TEST-- +PDO MySQL: reusing a persistent connection resets its session state (COM_RESET_CONNECTION) +--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); +$con1 = $db1->query('SELECT CONNECTION_ID()')->fetchColumn(); + +$db1->exec('SET @test_var = 42'); +$db1->exec('CREATE TEMPORARY TABLE pdo_reset_tmp (id INT)'); + +$db1->exec('DROP TABLE IF EXISTS pdo_reset_trx'); +$db1->exec('CREATE TABLE pdo_reset_trx (id INT) ENGINE=InnoDB'); + +$db1->beginTransaction(); +$db1->exec('INSERT INTO pdo_reset_trx VALUES (1)'); + +// 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(); + +// 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 ($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!"; +?> +--EXPECT-- +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_odbc/odbc_driver.c b/ext/pdo_odbc/odbc_driver.c index dbdb0066578b..90fbba0e3775 100644 --- a/ext/pdo_odbc/odbc_driver.c +++ b/ext/pdo_odbc/odbc_driver.c @@ -467,7 +467,8 @@ static const struct pdo_dbh_methods odbc_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 52aae986dee1..83554df258ca 100644 --- a/ext/pdo_pgsql/pgsql_driver.c +++ b/ext/pdo_pgsql/pgsql_driver.c @@ -1399,7 +1399,8 @@ static const struct pdo_dbh_methods pgsql_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 39efa02dd583..1d2b03a03fac 100644 --- a/ext/pdo_sqlite/sqlite_driver.c +++ b/ext/pdo_sqlite/sqlite_driver.c @@ -787,7 +787,8 @@ static const struct pdo_dbh_methods sqlite_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)