diff --git a/ext/pdo/pdo_dbh.c b/ext/pdo/pdo_dbh.c index 58bb3b1727f6..9ccaebd5e8bb 100644 --- a/ext/pdo/pdo_dbh.c +++ b/ext/pdo/pdo_dbh.c @@ -1606,7 +1606,7 @@ static void pdo_dbh_free_storage(zend_object *std) dbh->in_txn = false; } - if (dbh->is_persistent && dbh->driver_data && dbh->methods && dbh->methods->persistent_shutdown) { + if (dbh->is_persistent && dbh->refcount <= 2 && dbh->driver_data && dbh->methods && dbh->methods->persistent_shutdown) { dbh->methods->persistent_shutdown(dbh); } zend_object_std_dtor(std); diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c index cbfd55fc1aa6..548a47c3ee83 100644 --- a/ext/pdo_pgsql/pgsql_driver.c +++ b/ext/pdo_pgsql/pgsql_driver.c @@ -251,6 +251,11 @@ static void pgsql_handle_closer(pdo_dbh_t *dbh) /* {{{ */ pefree(H->lob_streams, dbh->is_persistent); H->lob_streams = NULL; } + if (H->prepared_stmts) { + zend_hash_destroy(H->prepared_stmts); + pefree(H->prepared_stmts, 1); + H->prepared_stmts = NULL; + } pdo_pgsql_cleanup_notice_callback(H); if (H->server) { PQfinish(H->server); @@ -1398,11 +1403,28 @@ static void pdo_pgsql_request_shutdown(pdo_dbh_t *dbh) pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data; if(H->server) { - res = PQexec(H->server, "DISCARD ALL"); - - if(res) { + zend_string *name; + + ZEND_HASH_MAP_FOREACH_STR_KEY(H->prepared_stmts, name) { +#ifndef HAVE_PQCLOSEPREPARED + char *q; + spprintf(&q, 0, "DEALLOCATE %s", ZSTR_VAL(name)); + res = PQexec(H->server, q); + efree(q); +#else + res = PQclosePrepared(H->server, ZSTR_VAL(name)); +#endif PQclear(res); + } ZEND_HASH_FOREACH_END(); + zend_hash_clean(H->prepared_stmts); + + res = PQexec(H->server, "CLOSE ALL; SET SESSION AUTHORIZATION DEFAULT; RESET ALL; UNLISTEN *; " + "SELECT pg_advisory_unlock_all(); DISCARD TEMP; DISCARD SEQUENCES"); + + if (PQresultStatus(res) != PGRES_COMMAND_OK && PQstatus(H->server) == CONNECTION_OK) { + PQreset(H->server); } + PQclear(res); } } @@ -1523,6 +1545,10 @@ static int pdo_pgsql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{ H->server = PQconnectdb(ZSTR_VAL(conn_str.s)); H->lob_streams = (HashTable *) pemalloc(sizeof(HashTable), dbh->is_persistent); zend_hash_init(H->lob_streams, 0, NULL, NULL, 1); + if (dbh->is_persistent) { + H->prepared_stmts = (HashTable *) pemalloc(sizeof(HashTable), 1); + zend_hash_init(H->prepared_stmts, 0, NULL, NULL, 1); + } if (tmp_user) { zend_string_release_ex(tmp_user, 0); diff --git a/ext/pdo_pgsql/pgsql_statement.c b/ext/pdo_pgsql/pgsql_statement.c index 2f0725534937..de7d03bc7264 100644 --- a/ext/pdo_pgsql/pgsql_statement.c +++ b/ext/pdo_pgsql/pgsql_statement.c @@ -190,9 +190,11 @@ static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode) res = PQclosePrepared(H->server, S->stmt_name); #endif if (res) { + if (H->prepared_stmts && PQresultStatus(res) == PGRES_COMMAND_OK) { + zend_hash_str_del(H->prepared_stmts, S->stmt_name, strlen(S->stmt_name)); + } PQclear(res); } - S->is_prepared = false; } } @@ -339,6 +341,9 @@ static int pgsql_stmt_execute(pdo_stmt_t *stmt) case PGRES_TUPLES_OK: /* it worked */ S->is_prepared = true; + if (H->prepared_stmts) { + zend_hash_str_add_empty_element(H->prepared_stmts, S->stmt_name, strlen(S->stmt_name)); + } PQclear(S->result); S->result = NULL; break; diff --git a/ext/pdo_pgsql/php_pdo_pgsql_int.h b/ext/pdo_pgsql/php_pdo_pgsql_int.h index dc71d5ae7a5f..9ca729cd44a5 100644 --- a/ext/pdo_pgsql/php_pdo_pgsql_int.h +++ b/ext/pdo_pgsql/php_pdo_pgsql_int.h @@ -45,6 +45,7 @@ typedef struct { bool emulate_prepares; bool disable_prepares; HashTable *lob_streams; + HashTable *prepared_stmts; zend_fcall_info_cache *notice_callback; bool default_fetching_laziness; zend_long default_chunk_size; diff --git a/ext/pdo_pgsql/tests/gh24461.phpt b/ext/pdo_pgsql/tests/gh24461.phpt new file mode 100644 index 000000000000..600a58f51230 --- /dev/null +++ b/ext/pdo_pgsql/tests/gh24461.phpt @@ -0,0 +1,40 @@ +--TEST-- +GH-24461 (Persistent connections: session reset keeps prepared statements) +--EXTENSIONS-- +pdo_pgsql +--SKIPIF-- + +--FILE-- + true])); + +require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc'; + +$pdo1 = PDOTest::test_factory(__DIR__ . '/common.phpt'); +$pid1 = (int) $pdo1->query('SELECT pg_backend_pid()')->fetchColumn(); + +$pdo1->exec("SET application_name = 'gh24461'"); +$pdo1->exec('CREATE TEMP TABLE gh24461_tmp (i int)'); +$pdo1->exec('PREPARE gh24461_stmt AS SELECT 42'); + +$pdo1 = null; + +$pdo2 = PDOTest::test_factory(__DIR__ . '/common.phpt'); +$pid2 = (int) $pdo2->query('SELECT pg_backend_pid()')->fetchColumn(); +var_dump($pid1 === $pid2); + +var_dump($pdo2->query('SHOW application_name')->fetchColumn()); +var_dump($pdo2->query("SELECT to_regclass('pg_temp.gh24461_tmp')")->fetchColumn()); +var_dump($pdo2->query('EXECUTE gh24461_stmt')->fetchColumn()); + +$pdo2->exec('DEALLOCATE gh24461_stmt'); +?> +--EXPECT-- +bool(true) +string(0) "" +NULL +string(2) "42" diff --git a/ext/pdo_pgsql/tests/gh24461_aborted_transaction_close.phpt b/ext/pdo_pgsql/tests/gh24461_aborted_transaction_close.phpt new file mode 100644 index 000000000000..9c07b739b9b7 --- /dev/null +++ b/ext/pdo_pgsql/tests/gh24461_aborted_transaction_close.phpt @@ -0,0 +1,40 @@ +--TEST-- +GH-24461 (Persistent connections: statements closed inside an aborted transaction are closed on reset) +--EXTENSIONS-- +pdo_pgsql +--SKIPIF-- + +--FILE-- + true, PDO::ATTR_EMULATE_PREPARES => false])); + +require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc'; + +$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt'); +$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$pid1 = (int) $pdo->query('SELECT pg_backend_pid()')->fetchColumn(); +$pdo->beginTransaction(); +$stmt = $pdo->prepare("SELECT 'gh24461_aborted'"); +$stmt->execute(); +try { + $pdo->exec('SELECT 1/0'); +} catch (PDOException $e) { +} +$stmt = null; +$pdo->rollBack(); +$pdo = null; + +$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt'); +$pid2 = (int) $pdo->query('SELECT pg_backend_pid()')->fetchColumn(); +var_dump($pid1 === $pid2); +var_dump($pdo->query("SELECT count(*) FROM pg_prepared_statements WHERE statement = 'SELECT ''gh24461_aborted'''")->fetchColumn()); +?> +--CREDITS-- +Artem Ukrainskiy +--EXPECT-- +bool(true) +string(1) "0" diff --git a/ext/pdo_pgsql/tests/gh24461_leftover_statement.phpt b/ext/pdo_pgsql/tests/gh24461_leftover_statement.phpt new file mode 100644 index 000000000000..a418e3ad358f --- /dev/null +++ b/ext/pdo_pgsql/tests/gh24461_leftover_statement.phpt @@ -0,0 +1,37 @@ +--TEST-- +GH-24461 (Persistent connections: statements outliving their PDO object are closed on reset) +--EXTENSIONS-- +pdo_pgsql +--SKIPIF-- + +--FILE-- + true])); + +require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc'; + +class HoldingPDO extends PDO { + public $stmt; +} + +$pdo1 = PDOTest::test_factory(__DIR__ . '/common.phpt', HoldingPDO::class); +$pid1 = (int) $pdo1->query('SELECT pg_backend_pid()')->fetchColumn(); +$pdo1->stmt = $pdo1->prepare("SELECT 'gh24461'"); +$pdo1->stmt->execute(); + +unset($pdo1); +var_dump(gc_collect_cycles() > 0); + +$pdo2 = PDOTest::test_factory(__DIR__ . '/common.phpt'); +$pid2 = (int) $pdo2->query('SELECT pg_backend_pid()')->fetchColumn(); +var_dump($pid1 === $pid2); +var_dump($pdo2->query("SELECT count(*) FROM pg_prepared_statements WHERE statement = 'SELECT ''gh24461'''")->fetchColumn()); +?> +--EXPECT-- +bool(true) +bool(true) +string(1) "0" diff --git a/ext/pdo_pgsql/tests/gh24461_shared_connection.phpt b/ext/pdo_pgsql/tests/gh24461_shared_connection.phpt new file mode 100644 index 000000000000..f341beecb081 --- /dev/null +++ b/ext/pdo_pgsql/tests/gh24461_shared_connection.phpt @@ -0,0 +1,46 @@ +--TEST-- +GH-24461 (Persistent connections: session state is not reset while another PDO object still uses the connection) +--EXTENSIONS-- +pdo_pgsql +--SKIPIF-- + +--FILE-- + true])); + +require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc'; + +$pdo1 = PDOTest::test_factory(__DIR__ . '/common.phpt'); +$pdo2 = PDOTest::test_factory(__DIR__ . '/common.phpt'); + +$pid1 = (int) $pdo1->query('SELECT pg_backend_pid()')->fetchColumn(); +$pid2 = (int) $pdo2->query('SELECT pg_backend_pid()')->fetchColumn(); +var_dump($pid1 === $pid2); + +$pdo2->exec("SET application_name = 'gh24461'"); +$pdo2->exec('CREATE TEMP TABLE gh24461_shared (i int)'); +$pdo2->query('SELECT pg_advisory_lock(24461)'); +$stmt = $pdo2->prepare('SELECT 42'); +$stmt->execute(); +$stmt->closeCursor(); + +$pdo1 = null; + +var_dump($pdo2->query('SHOW application_name')->fetchColumn()); +var_dump($pdo2->query('SELECT count(*) FROM gh24461_shared')->fetchColumn()); +var_dump($pdo2->query("SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND objid = 24461 AND pid = pg_backend_pid()")->fetchColumn()); +$stmt->execute(); +var_dump($stmt->fetchColumn()); + +$pdo2->query('SELECT pg_advisory_unlock_all()'); +?> +--EXPECT-- +bool(true) +string(7) "gh24461" +string(1) "0" +string(1) "1" +string(2) "42"