Commit 19deb91002a for php.net

commit 19deb91002a53958893903aa3315e30e3285e614
Author: 武田 憲太郎 <takeda@youmind.jp>
Date:   Mon Nov 24 12:03:38 2025 +0000

    pdo_pgsql: Reset persistent session state on disconnect-equivalent processing

    close GH-20572

diff --git a/NEWS b/NEWS
index 1ec90015ef2..af1bb92003d 100644
--- a/NEWS
+++ b/NEWS
@@ -30,6 +30,10 @@ PHP                                                                        NEWS
   . Fixed bug GH-20051 (apache2 shutdowns when restart is requested during
     preloading). (Arnaud, welcomycozyhom)

+- PDO_PGSQL:
+  . Clear session-local state disconnect-equivalent processing.
+    (KentarouTakeda)
+
 - Phar:
   . Support reference values in Phar::mungServer(). (ndossche)
   . Invalid values now throw in Phar::mungServer() instead of being silently
diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c
index be865c1f868..44f70e00236 100644
--- a/ext/pdo_pgsql/pgsql_driver.c
+++ b/ext/pdo_pgsql/pgsql_driver.c
@@ -1340,6 +1340,20 @@ static const zend_function_entry *pdo_pgsql_get_driver_methods(pdo_dbh_t *dbh, i
 	}
 }

+static void pdo_pgsql_request_shutdown(pdo_dbh_t *dbh)
+{
+	PGresult *res;
+	pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
+
+	if(H->server) {
+		res = PQexec(H->server, "DISCARD ALL");
+
+		if(res) {
+			PQclear(res);
+		}
+	}
+}
+
 static bool pdo_pgsql_set_attr(pdo_dbh_t *dbh, zend_long attr, zval *val)
 {
 	bool bval;
@@ -1383,7 +1397,7 @@ static const struct pdo_dbh_methods pgsql_methods = {
 	pdo_pgsql_get_attribute,
 	pdo_pgsql_check_liveness,	/* check_liveness */
 	pdo_pgsql_get_driver_methods,  /* get_driver_methods */
-	NULL,
+	pdo_pgsql_request_shutdown,
 	pgsql_handle_in_transaction,
 	NULL, /* get_gc */
 	pdo_pgsql_scanner
diff --git a/ext/pdo_pgsql/tests/session_state_reset.phpt b/ext/pdo_pgsql/tests/session_state_reset.phpt
new file mode 100644
index 00000000000..ad892bb0da5
--- /dev/null
+++ b/ext/pdo_pgsql/tests/session_state_reset.phpt
@@ -0,0 +1,55 @@
+--TEST--
+Persistent connections: session state reset when performing disconnect-equivalent processing (general case)
+--EXTENSIONS--
+pdo_pgsql
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+putenv('PDOTEST_ATTR='.serialize([PDO::ATTR_PERSISTENT => true]));
+
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+
+$pdo1 = PDOTest::test_factory(__DIR__ . '/common.phpt');
+
+$pid1 = (int)$pdo1
+    ->query('select pg_backend_pid()::int;')
+    ->fetchColumn(0);
+
+$defaultValue = (int)$pdo1
+    ->query('show log_min_duration_statement;')
+    ->fetchColumn(0);
+
+$setValue = $defaultValue + 1;
+
+$pdo1->exec("set log_min_duration_statement = {$setValue};");
+
+$pdo1 = null;
+
+$pdo2 = PDOTest::test_factory(__DIR__ . '/common.phpt');
+
+$pid2 = (int)$pdo2
+    ->query('select pg_backend_pid()::int;')
+    ->fetchColumn(0);
+
+assert($pid1 === $pid2);
+
+$expectedValue = (int)$pdo2
+    ->query('show log_min_duration_statement;')
+    ->fetchColumn(0);
+
+echo "defaultValue: {$defaultValue}\n";
+echo "setValue: {$setValue}\n";
+echo "expectedValue: {$expectedValue}\n";
+echo "expected value should be reset to default: " . (($expectedValue === $defaultValue) ? 'success' : 'failure') . "\n";
+
+?>
+--EXPECTF--
+defaultValue: %i
+setValue: %d
+expectedValue: %i
+expected value should be reset to default: success
diff --git a/ext/pdo_pgsql/tests/session_state_reset_advisory_lock.phpt b/ext/pdo_pgsql/tests/session_state_reset_advisory_lock.phpt
new file mode 100644
index 00000000000..3435f6b7b40
--- /dev/null
+++ b/ext/pdo_pgsql/tests/session_state_reset_advisory_lock.phpt
@@ -0,0 +1,51 @@
+--TEST--
+Persistent connections: session state reset when performing disconnect-equivalent processing (advisory lock case)
+--EXTENSIONS--
+pdo_pgsql
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+putenv('PDOTEST_ATTR='.serialize([PDO::ATTR_PERSISTENT => true]));
+
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+
+$pdo1 = PDOTest::test_factory(__DIR__ . '/common.phpt');
+
+$pid1 = (int)$pdo1
+    ->query('select pg_backend_pid()::int;')
+    ->fetchColumn(0);
+
+$lockResult1 = (bool)$pdo1
+    ->query('select pg_try_advisory_lock(42)::int;')
+    ->fetchColumn(0);
+
+$pdo1 = null;
+
+$dsn = getenv('PDO_PGSQL_TEST_DSN');
+$dsn .= ';';
+putenv('PDO_PGSQL_TEST_DSN='.$dsn);
+
+$pdo2 = PDOTest::test_factory(__DIR__ . '/common.phpt');
+
+$pid2 = (int)$pdo2
+    ->query('select pg_backend_pid()::int;')
+    ->fetchColumn(0);
+
+assert($pid1 !== $pid2);
+
+$lockResult2 = (bool)$pdo2
+    ->query('select pg_try_advisory_lock(42)::int;')
+    ->fetchColumn(0);
+
+echo "lock1: " . ($lockResult1 ? 'success' : 'failure') . "\n";
+echo "lock2: " . ($lockResult2 ? 'success' : 'failure') . "\n";
+
+?>
+--EXPECT--
+lock1: success
+lock2: success
diff --git a/ext/pdo_pgsql/tests/session_state_reset_after_interrupted.phpt b/ext/pdo_pgsql/tests/session_state_reset_after_interrupted.phpt
new file mode 100644
index 00000000000..fdafe8a00fc
--- /dev/null
+++ b/ext/pdo_pgsql/tests/session_state_reset_after_interrupted.phpt
@@ -0,0 +1,52 @@
+--TEST--
+Persistent connections: session state reset after backend termination (interrupted case)
+--EXTENSIONS--
+pdo_pgsql
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+putenv('PDOTEST_ATTR='.serialize([PDO::ATTR_PERSISTENT => true]));
+
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+
+$pdo1 = PDOTest::test_factory(__DIR__ . '/common.phpt');
+
+$pid1 = (int)$pdo1
+    ->query('select pg_backend_pid()::int;')
+    ->fetchColumn(0);
+
+$pid1 = (int)$pdo1
+    ->query('select pg_backend_pid()::int;')
+    ->fetchColumn(0);
+
+$dsn = getenv('PDO_PGSQL_TEST_DSN');
+$dsn .= ';';
+putenv('PDO_PGSQL_TEST_DSN='.$dsn);
+
+$pdo2 = PDOTest::test_factory(__DIR__ . '/common.phpt');
+
+$pid2 = (int)$pdo2
+    ->query('select pg_backend_pid()::int;')
+    ->fetchColumn(0);
+
+assert($pid1 !== $pid2);
+
+$terminateResult = (bool)$pdo2
+    ->query("select pg_terminate_backend({$pid1})::int")
+    ->fetchColumn(0);
+
+// Disconnect after being terminated by another connection
+$pdo1 = null;
+
+echo 'pid of pdo1: ' . $pid1 . "\n";
+echo 'terminate result of pdo1 by pdo2: ' . ($terminateResult ? 'success' : 'failure') . "\n";
+
+?>
+--EXPECTF--
+pid of pdo1: %d
+terminate result of pdo1 by pdo2: success