Commit 197bdc0a405 for php.net

commit 197bdc0a40586be9583c5b77de35e9900b6a912e
Author: 武田 憲太郎 <takeda@youmind.jp>
Date:   Tue Aug 25 23:00:34 2026 +0000

    ext/pdo_pgsql: Fix PDO::CURSOR_SCROLL statements failing under lazy fetching

    A statement prepared with PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL and lazy
    fetching (PDO::ATTR_PREFETCH => 0) failed at execute() with
    SQLSTATE[HY000]: General error: 7 and no message. Either option alone
    worked.

    A cursor does not stream its result, but S->is_unbuffered stayed set, so
    execute() called PQgetResult() with nothing in flight. Clear the flag when
    the statement has a cursor.

    Close GH-23471

diff --git a/NEWS b/NEWS
index 149fea34707..466321cf102 100644
--- a/NEWS
+++ b/NEWS
@@ -48,6 +48,10 @@ PHP                                                                        NEWS
   . Fixed a leak when a persistent connection failed a liveness check
     with no other live PDO handle. (iliaal)

+- PDO_PGSQL:
+  . Fixed PDO::CURSOR_SCROLL statements failing under lazy fetching
+    (PDO::ATTR_PREFETCH => 0). (KentarouTakeda)
+
 - Phar:
   . Fixed bug GH-23418 (Use-after-free when looking up mounted directories).
     (Weilin Du)
diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c
index 54b2e25f72f..060b8d68776 100644
--- a/ext/pdo_pgsql/pgsql_driver.c
+++ b/ext/pdo_pgsql/pgsql_driver.c
@@ -287,7 +287,16 @@ static bool pgsql_handle_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *
 	scrollable = pdo_attr_lval(driver_options, PDO_ATTR_CURSOR,
 		PDO_CURSOR_FWDONLY) == PDO_CURSOR_SCROLL;

+	S->is_unbuffered =
+		driver_options
+		&& (val = zend_hash_index_find(Z_ARRVAL_P(driver_options), PDO_ATTR_PREFETCH))
+		&& pdo_get_long_param(&lval, val)
+		? !lval
+		: H->default_fetching_laziness
+	;
+
 	if (scrollable) {
+		S->is_unbuffered = false;
 		if (S->cursor_name) {
 			efree(S->cursor_name);
 		}
@@ -312,14 +321,6 @@ static bool pgsql_handle_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *
 		stmt->named_rewrite_template = "$%d";
 	}

-	S->is_unbuffered =
-		driver_options
-		&& (val = zend_hash_index_find(Z_ARRVAL_P(driver_options), PDO_ATTR_PREFETCH))
-		&& pdo_get_long_param(&lval, val)
-		? !lval
-		: H->default_fetching_laziness
-	;
-
 	ret = pdo_parse_params(stmt, sql, &nsql);

 	if (ret == -1) {
diff --git a/ext/pdo_pgsql/tests/cursor_scroll_lazy_fetch.phpt b/ext/pdo_pgsql/tests/cursor_scroll_lazy_fetch.phpt
new file mode 100644
index 00000000000..df4be41b853
--- /dev/null
+++ b/ext/pdo_pgsql/tests/cursor_scroll_lazy_fetch.phpt
@@ -0,0 +1,33 @@
+--TEST--
+PDO PgSQL a scrollable cursor is unaffected by lazy fetching
+--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);
+
+$sql = "SELECT * FROM generate_series(1, 3)";
+$scrollable = [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL];
+
+$stmt = $pdo->prepare($sql, $scrollable + [PDO::ATTR_PREFETCH => 0]);
+$stmt->execute();
+echo 'lazy on the statement: ', implode(',', $stmt->fetchAll(PDO::FETCH_COLUMN)), PHP_EOL;
+
+$pdo->setAttribute(PDO::ATTR_PREFETCH, 0);
+$stmt = $pdo->prepare($sql, $scrollable);
+$stmt->execute();
+echo 'lazy on the connection: ', implode(',', $stmt->fetchAll(PDO::FETCH_COLUMN)), PHP_EOL;
+
+?>
+--EXPECT--
+lazy on the statement: 1,2,3
+lazy on the connection: 1,2,3