[php-src] master: Merge branch 'PHP-8.5'

David Carlier <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: David Carlier (devnexen)
Date: 2026-08-11T19:46:03+01:00

Commit: https://github.com/php/php-src/commit/61c601c5f81fc56d16a2fc2a021b12fec4da405f
Raw diff: https://github.com/php/php-src/commit/61c601c5f81fc56d16a2fc2a021b12fec4da405f.diff

Merge branch 'PHP-8.5'

* PHP-8.5:
  ext/pdo_pgsql: Fix several lazy fetch defects

Changed paths:
  A  ext/pdo_pgsql/tests/lazy_fetch_cancel.phpt
  A  ext/pdo_pgsql/tests/lazy_fetch_copy.phpt
  A  ext/pdo_pgsql/tests/lazy_fetch_drain.phpt
  A  ext/pdo_pgsql/tests/lazy_fetch_takeover.phpt
  A  ext/pdo_pgsql/tests/lazy_fetch_takeover_buffered.phpt
  M  ext/pdo_pgsql/pgsql_statement.c


Diff:

diff --git a/ext/pdo_pgsql/pgsql_statement.c b/ext/pdo_pgsql/pgsql_statement.c
index 1620544556b6..4deba242bbb6 100644
--- a/ext/pdo_pgsql/pgsql_statement.c
+++ b/ext/pdo_pgsql/pgsql_statement.c
@@ -64,12 +64,12 @@ static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode)
 {
 	pdo_pgsql_db_handle *H = S->H;
 
-	if (S->is_running_unbuffered && S->result && (fin_mode & FIN_ABORT)) {
+	/* a buffered query may have already drained this statement's stream */
+	if (S->is_running_unbuffered && H->running_stmt == S && S->result && (fin_mode & FIN_ABORT)) {
 		PGcancel *cancel = PQgetCancel(H->server);
 		char errbuf[256];
 		PQcancel(cancel, errbuf, 256);
 		PQfreeCancel(cancel);
-		S->is_running_unbuffered = false;
 	}
 
 	if (S->result) {
@@ -78,7 +78,7 @@ static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode)
 		S->result = NULL;
 	}
 
-	if (S->is_running_unbuffered) {
+	if (S->is_running_unbuffered && H->running_stmt == S) {
 		/* https://postgresql.org/docs/current/libpq-async.html:
 		 * "PQsendQuery cannot be called again until PQgetResult has returned NULL"
 		 * And as all single-row functions are connection-wise instead of statement-wise,
@@ -88,8 +88,35 @@ static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode)
 		//       instead of discarding results we could store them to their statement
 		//       so that their fetch() will get them (albeit not in lazy mode anymore).
 		while ((S->result = PQgetResult(H->server))) {
+			ExecStatusType status = PQresultStatus(S->result);
+
 			PQclear(S->result);
 			S->result = NULL;
+
+			/* PQgetResult() keeps handing out the same result while the
+			 * connection is copying: only these calls can end it */
+			if (status == PGRES_COPY_IN || status == PGRES_COPY_BOTH) {
+				/* fail a copy in, so that abandoning a statement cannot
+				 * commit it; a replication stream only accepts a clean end */
+				const char *error = status == PGRES_COPY_IN
+					? "COPY terminated by PDO"
+					: NULL;
+
+				if (PQputCopyEnd(H->server, error) < 0) {
+					break;
+				}
+			}
+			if (status == PGRES_COPY_OUT || status == PGRES_COPY_BOTH) {
+				char *buf;
+				int nbytes;
+
+				while ((nbytes = PQgetCopyData(H->server, &buf, 0)) > 0) {
+					PQfreemem(buf);
+				}
+				if (nbytes < -1) {
+					break;
+				}
+			}
 		}
 		S->is_running_unbuffered = false;
 	}
@@ -111,9 +138,6 @@ static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode)
 		}
 
 		S->is_prepared = false;
-		if (H->running_stmt == S) {
-			H->running_stmt = NULL;
-		}
 	}
 }
 
@@ -124,6 +148,10 @@ static int pgsql_stmt_dtor(pdo_stmt_t *stmt)
 
 	pgsql_stmt_finish(S, FIN_DISCARD|(server_obj_usable ? FIN_CLOSE|FIN_ABORT : 0));
 
+	if (server_obj_usable && S->H->running_stmt == S) {
+		S->H->running_stmt = NULL;
+	}
+
 	if (S->stmt_name) {
 		efree(S->stmt_name);
 		S->stmt_name = NULL;
@@ -559,7 +587,7 @@ static int pgsql_stmt_fetch(pdo_stmt_t *stmt,
 			return 0;
 		}
 	} else {
-		if (S->is_running_unbuffered && S->current_row >= stmt->row_count) {
+		if (S->is_running_unbuffered && S->H->running_stmt == S && S->current_row >= stmt->row_count) {
 			ExecStatusType status;
 
 			/* @todo in unbuffered mode, PQ allows multiple queries to be passed:
@@ -588,12 +616,12 @@ static int pgsql_stmt_fetch(pdo_stmt_t *stmt,
 			S->current_row = 0;
 
 			if (!stmt->row_count) {
-				S->is_running_unbuffered = false;
 				/* libpq requires looping until getResult returns null */
 				pgsql_stmt_finish(S, 0);
 			}
 		}
-		if (S->current_row < stmt->row_count) {
+		/* another statement may have taken over and freed the result */
+		if (S->result && S->current_row < stmt->row_count) {
 			S->current_row++;
 			return 1;
 		} else {
diff --git a/ext/pdo_pgsql/tests/lazy_fetch_cancel.phpt b/ext/pdo_pgsql/tests/lazy_fetch_cancel.phpt
new file mode 100644
index 000000000000..7968c2653206
--- /dev/null
+++ b/ext/pdo_pgsql/tests/lazy_fetch_cancel.phpt
@@ -0,0 +1,36 @@
+--TEST--
+PDO PgSQL an abandoned lazy fetch frees the connection without a prepared statement
+--EXTENSIONS--
+pdo_pgsql
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt');
+$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+foreach ([
+    'PDO::ATTR_EMULATE_PREPARES' => [PDO::ATTR_EMULATE_PREPARES => true],
+    'Pdo\Pgsql::ATTR_DISABLE_PREPARES' => [Pdo\Pgsql::ATTR_DISABLE_PREPARES => true],
+] as $label => $options) {
+    $options[PDO::ATTR_PREFETCH] = 0;
+
+    $stmt = $pdo->prepare("VALUES (1), (2)", $options);
+    $stmt->execute();
+    $stmt = null;
+
+    $stmt = $pdo->prepare("VALUES (1), (2)", $options);
+    $stmt->execute();
+    echo "$label: ";
+    var_dump((bool) $stmt->fetchAll());
+}
+?>
+--EXPECT--
+PDO::ATTR_EMULATE_PREPARES: bool(true)
+Pdo\Pgsql::ATTR_DISABLE_PREPARES: bool(true)
diff --git a/ext/pdo_pgsql/tests/lazy_fetch_copy.phpt b/ext/pdo_pgsql/tests/lazy_fetch_copy.phpt
new file mode 100644
index 000000000000..91321e2bcde2
--- /dev/null
+++ b/ext/pdo_pgsql/tests/lazy_fetch_copy.phpt
@@ -0,0 +1,35 @@
+--TEST--
+PDO PgSQL a lazy fetch left in a COPY does not hang the connection cleanup
+--EXTENSIONS--
+pdo_pgsql
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt');
+$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
+$pdo->setAttribute(PDO::ATTR_PREFETCH, 0);
+$pdo->exec("CREATE TEMPORARY TABLE lazy_fetch_copy (i int)");
+
+foreach ([
+    'COPY OUT' => "COPY (SELECT 1) TO STDOUT",
+    'COPY IN' => "COPY lazy_fetch_copy FROM STDIN",
+] as $label => $sql) {
+    $copy = $pdo->prepare($sql);
+    $copy->execute();
+
+    $stmt = $pdo->prepare("VALUES (1), (2)");
+    $stmt->execute();
+    echo "$label: ";
+    var_dump((bool) $stmt->fetchAll());
+}
+?>
+--EXPECT--
+COPY OUT: bool(true)
+COPY IN: bool(true)
diff --git a/ext/pdo_pgsql/tests/lazy_fetch_drain.phpt b/ext/pdo_pgsql/tests/lazy_fetch_drain.phpt
new file mode 100644
index 000000000000..a650628d3cce
--- /dev/null
+++ b/ext/pdo_pgsql/tests/lazy_fetch_drain.phpt
@@ -0,0 +1,36 @@
+--TEST--
+PDO PgSQL a drained lazy fetch frees the connection without a prepared statement
+--EXTENSIONS--
+pdo_pgsql
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt');
+$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+foreach ([
+    'PDO::ATTR_EMULATE_PREPARES' => [PDO::ATTR_EMULATE_PREPARES => true],
+    'Pdo\Pgsql::ATTR_DISABLE_PREPARES' => [Pdo\Pgsql::ATTR_DISABLE_PREPARES => true],
+] as $label => $options) {
+    $options[PDO::ATTR_PREFETCH] = 0;
+
+    $stmt = $pdo->prepare("VALUES (1), (2)", $options);
+    $stmt->execute();
+    $stmt->fetchAll();
+
+    $stmt = $pdo->prepare("VALUES (1), (2)", $options);
+    $stmt->execute();
+    echo "$label: ";
+    var_dump((bool) $stmt->fetchAll());
+}
+?>
+--EXPECT--
+PDO::ATTR_EMULATE_PREPARES: bool(true)
+Pdo\Pgsql::ATTR_DISABLE_PREPARES: bool(true)
diff --git a/ext/pdo_pgsql/tests/lazy_fetch_takeover.phpt b/ext/pdo_pgsql/tests/lazy_fetch_takeover.phpt
new file mode 100644
index 000000000000..eace678310de
--- /dev/null
+++ b/ext/pdo_pgsql/tests/lazy_fetch_takeover.phpt
@@ -0,0 +1,28 @@
+--TEST--
+PDO PgSQL a lazy fetch whose stream was taken over reports no leftover rows
+--EXTENSIONS--
+pdo_pgsql
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt');
+$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+$pdo->setAttribute(PDO::ATTR_PREFETCH, 0);
+
+$first = $pdo->prepare("VALUES (1), (2)");
+$first->execute();
+
+$pdo->prepare("VALUES (1), (2)")->execute();
+
+var_dump($first->fetchAll(PDO::FETCH_NUM));
+?>
+--EXPECT--
+array(0) {
+}
diff --git a/ext/pdo_pgsql/tests/lazy_fetch_takeover_buffered.phpt b/ext/pdo_pgsql/tests/lazy_fetch_takeover_buffered.phpt
new file mode 100644
index 000000000000..aa8c2aa86c4a
--- /dev/null
+++ b/ext/pdo_pgsql/tests/lazy_fetch_takeover_buffered.phpt
@@ -0,0 +1,38 @@
+--TEST--
+PDO PgSQL a lazy fetch stale after a buffered query does not read the next statement's rows
+--EXTENSIONS--
+pdo_pgsql
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
+$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt');
+$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+$first = $pdo->prepare("VALUES (1), (2), (3)", [PDO::ATTR_PREFETCH => 0]);
+$first->execute();
+$first->fetch();
+
+// a buffered query drains the stream but does not end $first's lazy fetch
+$pdo->prepare("VALUES (99)")->execute();
+
+$third = $pdo->prepare("VALUES (777), (888)", [PDO::ATTR_PREFETCH => 0]);
+$third->execute();
+
+var_dump($first->fetch(PDO::FETCH_NUM));
+var_dump($third->fetchAll(PDO::FETCH_COLUMN));
+?>
+--EXPECT--
+bool(false)
+array(2) {
+  [0]=>
+  string(3) "777"
+  [1]=>
+  string(3) "888"
+}
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.