On Wed, Sep 9, 2026 at 6:04 PM Dave Cramer <[email protected]> wrote:

> > It's looking much more finished
>
> Thanks for the review, and sorry for the long delay in getting back to
> it.  Attached is v5, rebased onto master and split into three patches:
>
>   v5-0001  the Bind side, as before but with your review comments
>            applied
>   v5-0002  the Execute side: a fetch direction and count
>   v5-0003  libpq support for the new Execute fields
>

I tried out this patch and have been stepping through it to understand
how it works. Still a WIP on my part (touches a lot of new-to-me
stuff...) but here's an interim review.

Builds and tests pass when applied atop 798bdcae. However I found some
situations that lead to either wrong behavior or crashes with assertions
enabled.

1. SCROLL uses the cached plan, which was never planned for scrolling.

Bind sets CURSOR_OPT_SCROLL on the portal but keeps the prepared
statement's cached plan, so PortalStart hands EXEC_FLAG_BACKWARD to a
plan that was never built for it. DECLARE avoids this by planning
with the cursor options, which adds a Material node when needed and
disables parallelism. On v5, a Bind with SCROLL on any of these
crashes at Bind time:

select 1                                 nodeResult.c:185
select count(*) from generate_series(1,5)   nodeAgg.c:3306
any hash join                            nodeHashjoin.c:844

I did a CTRL-F for Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)))
and there's a lot of executor nodes this would fail on.

Interestingly, the tests in v5 all end up as a Seq Scan, or a Sort over
one which allows for backwards scans. That's why they all pass but
something as simple as "SELECT 1" fails. Anything with an FDW would fail
too.

2. WITH HOLD on anything but a single SELECT crashes at COMMIT.

PersistHoldablePortal assumes PORTAL_ONE_SELECT, which is all DECLARE
can produce. Bind accepts HOLD on INSERT ... RETURNING, SHOW, or an
UPDATE, and the commit trips an assertion in PortalCreateHoldStore or
PersistHoldablePortal (a NULL dereference without assertions).

3. FOR UPDATE/SHARE isn't rejected with SCROLL or HOLD, though DECLARE
and SPI both reject it. HOLD + FOR UPDATE persists the locked rows at
commit and keeps fetching afterwards.

4. A fetch count of INT64_MIN overflows when DoPortalRunFetch negates
it. FETCH can't produce it because the grammar only takes a 32-bit int,
but the raw Int64 on the wire can.

5. The new libpq Bind functions don't range check nParams, so
nParams=70000 goes out as a 16-bit count of 4464.

Attached are two patches on top of v5. The first fixes the above and
the second adds libpq_pipeline tests for each case. v5 fails the new
tests and passes with the fix applied.

Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/
From ee40e89c960214a2f1ad32d0d4a713ee6be525a1 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <[email protected]>
Date: Thu, 10 Sep 2026 21:26:15 +0000
Subject: [PATCH v5 4/5] Fix crashes and missing checks in _pq_.cursor Bind and
 Execute handling

Replan a SCROLL portal when its cached plan is parallel or cannot scan
backwards, as PerformCursorOpen does.  Reject WITH HOLD unless the
portal holds a single SELECT.  Reject SCROLL and WITH HOLD with FOR
UPDATE/SHARE.  Reject a fetch count of INT64_MIN.  Check nParams in the
libpq cursor Bind functions.
---
 doc/src/sgml/protocol.sgml     |  13 ++++-
 src/backend/tcop/postgres.c    | 100 ++++++++++++++++++++++++++++++---
 src/interfaces/libpq/fe-exec.c |   7 +++
 3 files changed, 110 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 4ee69d51fe3..4f366d60763 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -353,10 +353,16 @@
          <member><literal>0x0004</literal> &mdash; WITH HOLD</member>
         </simplelist>
         SCROLL and NO SCROLL are mutually exclusive.
-        WITH HOLD is not permitted on unnamed portals.
+        WITH HOLD is not permitted on unnamed portals, and only for a
+        single <command>SELECT</command>, as for
+        <link linkend="sql-declare"><command>DECLARE</command></link>.
+        Neither SCROLL nor WITH HOLD is permitted with
+        <literal>FOR UPDATE</literal> or <literal>FOR SHARE</literal>.
         All other bits are reserved and must be zero.
         A portal is scrollable only if SCROLL is requested, so NO SCROLL is
-        accepted but never necessary.  A value of 0 requests no cursor options
+        accepted but never necessary.  SCROLL may cause the
+        statement to be replanned when its cached plan cannot be read
+        backwards.  A value of 0 requests no cursor options
         at all, and creates exactly the portal that a Bind message without this
         extension would create.
        </para>
@@ -5249,7 +5255,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
          Number of rows to fetch, interpreted as the count of the
          <link linkend="sql-fetch"><command>FETCH</command></link> command
          of the same direction would be.  The largest positive value
-         (<literal>0x7FFFFFFFFFFFFFFF</literal>) means <literal>ALL</literal>.
+         (<literal>0x7FFFFFFFFFFFFFFF</literal>) means <literal>ALL</literal>;
+         the most negative value is not allowed.
          This field must be zero if the fetch flags are 0.
         </para>
        </listitem>
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index cf43acd54b3..6fcf971a64a 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -41,6 +41,7 @@
 #include "commands/prepare.h"
 #include "commands/repack.h"
 #include "common/pg_prng.h"
+#include "executor/executor.h"
 #include "jit/jit.h"
 #include "libpq/libpq.h"
 #include "libpq/pqformat.h"
@@ -1671,6 +1672,7 @@ exec_bind_message(StringInfo input_message)
 	int16	   *rformats = NULL;
 	CachedPlanSource *psrc;
 	CachedPlan *cplan;
+	List	   *stmt_list;
 	Portal		portal;
 	char	   *query_string;
 	char	   *saved_stmt_name;
@@ -1682,6 +1684,7 @@ exec_bind_message(StringInfo input_message)
 	ParamsErrorCbData params_data;
 	ErrorContextCallback params_errcxt;
 	ListCell   *lc;
+	int			bind_ext_flags = 0;
 
 	/* Get the fixed part of the message */
 	portal_name = pq_getmsgstring(input_message);
@@ -2059,8 +2062,6 @@ exec_bind_message(StringInfo input_message)
 	 */
 	if (MyProcPort != NULL && MyProcPort->protocol_cursor_enabled)
 	{
-		int			bind_ext_flags;
-
 		bind_ext_flags = pq_getmsgint(input_message, 4);
 
 		/* Reject any bits we don't recognize */
@@ -2109,18 +2110,96 @@ exec_bind_message(StringInfo input_message)
 	 * assigned to the Portal, so it will be released at portal destruction.
 	 */
 	cplan = GetCachedPlan(psrc, params, NULL, NULL);
+	stmt_list = cplan->stmt_list;
+
+	/*
+	 * DECLARE CURSOR's restrictions on SCROLL and WITH HOLD depend on the
+	 * planned statement, so check them here.  Release the plan before
+	 * erroring out; nothing else would.
+	 */
+	if (bind_ext_flags & (PQ_BIND_CURSOR_SCROLL | PQ_BIND_CURSOR_HOLD))
+	{
+		PortalStrategy strategy = ChoosePortalStrategy(cplan->stmt_list);
+
+		/* PersistHoldablePortal can only cope with a single SELECT */
+		if ((bind_ext_flags & PQ_BIND_CURSOR_HOLD) &&
+			strategy != PORTAL_ONE_SELECT)
+		{
+			ReleaseCachedPlan(cplan, NULL);
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("WITH HOLD cursor option is only allowed for a SELECT statement")));
+		}
+
+		/* FOR UPDATE/SHARE, as in transformDeclareCursorStmt */
+		if (strategy == PORTAL_ONE_SELECT &&
+			linitial_node(PlannedStmt, cplan->stmt_list)->rowMarks != NIL)
+		{
+			if (bind_ext_flags & PQ_BIND_CURSOR_HOLD)
+			{
+				ReleaseCachedPlan(cplan, NULL);
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("WITH HOLD cursor option is not supported with FOR UPDATE/SHARE"),
+						 errdetail("Holdable cursors must be READ ONLY.")));
+			}
+			if (bind_ext_flags & PQ_BIND_CURSOR_SCROLL)
+			{
+				ReleaseCachedPlan(cplan, NULL);
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("SCROLL cursor option is not supported with FOR UPDATE/SHARE"),
+						 errdetail("Scrollable cursors must be READ ONLY.")));
+			}
+		}
+
+		/*
+		 * SCROLL starts the executor with EXEC_FLAG_BACKWARD, which only a
+		 * plan built with CURSOR_OPT_SCROLL is sure to support: the planner
+		 * then materializes anything that cannot scan backwards, and without
+		 * CURSOR_OPT_PARALLEL_OK there is no Gather.  The cached plan was
+		 * built without either, so if it is not usable as is, plan afresh as
+		 * PerformCursorOpen does and give the portal the plan outright.
+		 */
+		if ((bind_ext_flags & PQ_BIND_CURSOR_SCROLL) &&
+			strategy == PORTAL_ONE_SELECT)
+		{
+			PlannedStmt *pstmt = linitial_node(PlannedStmt, cplan->stmt_list);
+
+			if (pstmt->parallelModeNeeded ||
+				!ExecSupportsBackwardScan(pstmt->planTree))
+			{
+				int			cursor_options;
+
+				ReleaseCachedPlan(cplan, NULL);
+				cplan = NULL;
+
+				cursor_options = (psrc->cursor_options & ~CURSOR_OPT_PARALLEL_OK) |
+					CURSOR_OPT_SCROLL;
+				stmt_list = pg_plan_queries(copyObject(psrc->query_list),
+											psrc->query_string,
+											cursor_options, params);
+
+				/* the portal owns it, so it goes in its context */
+				oldContext = MemoryContextSwitchTo(portal->portalContext);
+				stmt_list = copyObject(stmt_list);
+				MemoryContextSwitchTo(oldContext);
+			}
+		}
+	}
 
 	/*
 	 * Now we can define the portal.
 	 *
 	 * DO NOT put any code that could possibly throw an error between the
-	 * above GetCachedPlan call and here.
+	 * above GetCachedPlan call and here, except the checks above, which
+	 * release the plan first.
 	 */
 	PortalDefineQuery(portal,
 					  saved_stmt_name,
 					  query_string,
 					  psrc->commandTag,
-					  cplan->stmt_list,
+					  stmt_list,
 					  cplan);
 
 	/* Portal is defined, set the plan ID based on its contents. */
@@ -2213,15 +2292,22 @@ fetch_count_wire_to_long(int64 count)
 	if (count == PQ_FETCH_ALL)
 		return FETCH_ALL;		/* == LONG_MAX */
 
+	/* DoPortalRunFetch negates negative counts; reject the one that overflows */
+	if (count == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("fetch count out of range")));
+
 	/*
 	 * The wire count is a full 64-bit integer, but "long" is only 32 bits
 	 * where SIZEOF_LONG < 8 (LLP64 Windows, ILP32 platforms).  There, a value
 	 * that does not fit would be silently truncated by the cast below, so
-	 * reject it instead.  Where "long" is 64 bits this test is always false,
-	 * so compile it out rather than emit a tautological comparison.
+	 * reject it instead, along with LONG_MIN itself for the reason above.
+	 * Where "long" is 64 bits this test is always false, so compile it out
+	 * rather than emit a tautological comparison.
 	 */
 #if SIZEOF_LONG < 8
-	if (count > LONG_MAX || count < LONG_MIN)
+	if (count > LONG_MAX || count <= LONG_MIN)
 		ereport(ERROR,
 				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
 				 errmsg("fetch count out of range for this platform")));
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 98dd831de4e..4e434798e7b 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -1968,6 +1968,13 @@ PQsendBindGuts(PGconn *conn,
 		return 0;
 	}
 
+	if (nParams < 0 || nParams > PQ_QUERY_PARAM_MAX_LIMIT)
+	{
+		libpq_append_conn_error(conn, "number of parameters must be between 0 and %d",
+								PQ_QUERY_PARAM_MAX_LIMIT);
+		return 0;
+	}
+
 	if (cursorOptions != 0 && !conn->protocol_cursor_enabled)
 	{
 		libpq_append_conn_error(conn,
-- 
2.17.1

From b0affb96df0eec357dac96f61bb12a412b112605 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <[email protected]>
Date: Thu, 10 Sep 2026 21:26:15 +0000
Subject: [PATCH v5 5/5] Add libpq_pipeline tests for _pq_.cursor edge cases

Cover SCROLL over SELECT 1, a hash join, an aggregate, and a parallel
plan, WITH HOLD on non-SELECT statements, FOR UPDATE with SCROLL and
WITH HOLD, a fetch count of INT64_MIN, and nParams out of range.
---
 .../modules/libpq_pipeline/libpq_pipeline.c   | 344 ++++++++++++++++++
 1 file changed, 344 insertions(+)

diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index 43f1cc38ea0..0d54fdbbd7c 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -24,6 +24,8 @@
 
 
 static void exit_nicely(PGconn *conn);
+static void confirm_fetch_result(PGconn *conn, const char *what, int nrows,
+								 int firstvalue);
 pg_noreturn static void pg_fatal_impl(int line, const char *fmt, ...)
 			pg_attribute_printf(2, 3);
 static bool process_result(PGconn *conn, PGresult *res, int results,
@@ -2514,6 +2516,303 @@ test_cursor_bind_dml(PGconn *conn)
 /*
  * Test client-side validation of cursor bind options.
  */
+/*
+ * Outside pipeline mode, so each rejection is its own command.
+ */
+static void
+expect_bind_rejected(PGconn *conn, const char *stmt, int cursorOptions,
+					 const char *what)
+{
+	if (PQsendBindWithCursorOptions(conn, stmt, 0, NULL, NULL, NULL, 0,
+									"rejected_portal", cursorOptions) != 1)
+		pg_fatal("%s: PQsendBindWithCursorOptions failed: %s",
+				 what, PQerrorMessage(conn));
+	consume_result_status(conn, PGRES_FATAL_ERROR);
+	consume_null_result(conn);
+}
+
+/*
+ * SCROLL and WITH HOLD are rejected with FOR UPDATE/SHARE; NO SCROLL is
+ * fine.
+ */
+static void
+test_cursor_bind_for_update(PGconn *conn)
+{
+	PGresult   *res;
+
+	fprintf(stderr, "test_cursor_bind_for_update... ");
+
+	res = PQexec(conn, "CREATE TEMP TABLE forupd_test AS SELECT generate_series(1, 3) AS id");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("CREATE TABLE failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	res = PQprepare(conn, "forupd_stmt",
+					"SELECT id FROM forupd_test ORDER BY id FOR UPDATE", 0, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("PREPARE failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	expect_bind_rejected(conn, "forupd_stmt", PQ_BIND_CURSOR_SCROLL,
+						 "SCROLL with FOR UPDATE");
+	expect_bind_rejected(conn, "forupd_stmt", PQ_BIND_CURSOR_HOLD,
+						 "HOLD with FOR UPDATE");
+	expect_bind_rejected(conn, "forupd_stmt",
+						 PQ_BIND_CURSOR_HOLD | PQ_BIND_CURSOR_SCROLL,
+						 "HOLD | SCROLL with FOR UPDATE");
+
+	res = PQexec(conn, "BEGIN");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("BEGIN failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	if (PQsendBindAndExecutePortal(conn, "forupd_stmt", 0, NULL, NULL, NULL, 0,
+								   "forupd_portal", PQ_BIND_CURSOR_NO_SCROLL,
+								   PQ_FETCH_FORWARD, 2) != 1)
+		pg_fatal("PQsendBindAndExecutePortal failed: %s", PQerrorMessage(conn));
+	confirm_fetch_result(conn, "NO SCROLL with FOR UPDATE", 2, 1);
+
+	res = PQexec(conn, "ROLLBACK");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("ROLLBACK failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	fprintf(stderr, "ok\n");
+}
+
+/*
+ * WITH HOLD is only allowed for a single SELECT.
+ */
+static void
+test_cursor_bind_hold_non_select(PGconn *conn)
+{
+	PGresult   *res;
+
+	fprintf(stderr, "test_cursor_bind_hold_non_select... ");
+
+	res = PQexec(conn, "CREATE TEMP TABLE holdns_test(id int)");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("CREATE TABLE failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	res = PQprepare(conn, "holdns_returning",
+					"INSERT INTO holdns_test VALUES (1) RETURNING id", 0, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("PREPARE failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	res = PQprepare(conn, "holdns_update",
+					"UPDATE holdns_test SET id = id", 0, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("PREPARE failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	res = PQprepare(conn, "holdns_utility", "SHOW work_mem", 0, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("PREPARE failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	res = PQprepare(conn, "holdns_modcte",
+					"WITH t AS (INSERT INTO holdns_test VALUES (2) RETURNING id) SELECT id FROM t",
+					0, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("PREPARE failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	expect_bind_rejected(conn, "holdns_returning", PQ_BIND_CURSOR_HOLD,
+						 "HOLD on INSERT RETURNING");
+	expect_bind_rejected(conn, "holdns_update", PQ_BIND_CURSOR_HOLD,
+						 "HOLD on UPDATE");
+	expect_bind_rejected(conn, "holdns_utility", PQ_BIND_CURSOR_HOLD,
+						 "HOLD on SHOW");
+	expect_bind_rejected(conn, "holdns_modcte", PQ_BIND_CURSOR_HOLD,
+						 "HOLD on SELECT with data-modifying CTE");
+
+	/* nothing was inserted */
+	res = PQexec(conn, "SELECT count(*) FROM holdns_test");
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("SELECT failed: %s", PQerrorMessage(conn));
+	if (strcmp(PQgetvalue(res, 0, 0), "0") != 0)
+		pg_fatal("expected 0 rows, got %s", PQgetvalue(res, 0, 0));
+	PQclear(res);
+
+	fprintf(stderr, "ok\n");
+}
+
+/*
+ * Bind stmt as a SCROLL portal and read it in every direction.
+ */
+static void
+check_scroll_plan(PGconn *conn, const char *stmt, const char *what)
+{
+	PGresult   *res;
+	char		first[64];
+	char		portal[64];
+
+	snprintf(portal, sizeof(portal), "%s_portal", stmt);
+
+	if (PQsendBindWithCursorOptions(conn, stmt, 0, NULL, NULL, NULL, 0,
+									portal, PQ_BIND_CURSOR_SCROLL) != 1)
+		pg_fatal("%s: PQsendBindWithCursorOptions failed: %s",
+				 what, PQerrorMessage(conn));
+	res = confirm_result_status(conn, PGRES_COMMAND_OK);
+	PQclear(res);
+	consume_null_result(conn);
+
+	/* forward 2, then backward 1 returns the first row either way */
+	if (PQsendExecutePortal(conn, portal, PQ_FETCH_FORWARD, 2) != 1)
+		pg_fatal("%s: forward fetch failed: %s", what, PQerrorMessage(conn));
+	res = confirm_result_status(conn, PGRES_TUPLES_OK);
+	if (PQntuples(res) < 1)
+		pg_fatal("%s: expected at least 1 row, got %d", what, PQntuples(res));
+	strlcpy(first, PQgetvalue(res, 0, 0), sizeof(first));
+	PQclear(res);
+	consume_null_result(conn);
+
+	if (PQsendExecutePortal(conn, portal, PQ_FETCH_BACKWARD, 1) != 1)
+		pg_fatal("%s: backward fetch failed: %s", what, PQerrorMessage(conn));
+	res = confirm_result_status(conn, PGRES_TUPLES_OK);
+	if (PQntuples(res) != 1)
+		pg_fatal("%s: expected 1 row from backward fetch, got %d",
+				 what, PQntuples(res));
+	if (strcmp(PQgetvalue(res, 0, 0), first) != 0)
+		pg_fatal("%s: expected \"%s\" from backward fetch, got \"%s\"",
+				 what, first, PQgetvalue(res, 0, 0));
+	PQclear(res);
+	consume_null_result(conn);
+
+	/* full forward pass; this is where a parallel plan broke */
+	if (PQsendExecutePortal(conn, portal, PQ_FETCH_FORWARD | PQ_FETCH_MOVE,
+							PQ_FETCH_ALL) != 1)
+		pg_fatal("%s: move failed: %s", what, PQerrorMessage(conn));
+	res = confirm_result_status(conn, PGRES_TUPLES_OK);
+	if (PQntuples(res) != 0)
+		pg_fatal("%s: expected no rows from move, got %d", what, PQntuples(res));
+	PQclear(res);
+	consume_null_result(conn);
+
+	if (PQsendExecutePortal(conn, portal, PQ_FETCH_BACKWARD, 1) != 1)
+		pg_fatal("%s: backward fetch failed: %s", what, PQerrorMessage(conn));
+	res = confirm_result_status(conn, PGRES_TUPLES_OK);
+	if (PQntuples(res) != 1)
+		pg_fatal("%s: expected 1 row from backward fetch at end, got %d",
+				 what, PQntuples(res));
+	PQclear(res);
+	consume_null_result(conn);
+
+	if (PQsendExecutePortal(conn, portal, PQ_FETCH_ABSOLUTE, 1) != 1)
+		pg_fatal("%s: absolute fetch failed: %s", what, PQerrorMessage(conn));
+	res = confirm_result_status(conn, PGRES_TUPLES_OK);
+	if (PQntuples(res) != 1)
+		pg_fatal("%s: expected 1 row from absolute fetch, got %d",
+				 what, PQntuples(res));
+	if (strcmp(PQgetvalue(res, 0, 0), first) != 0)
+		pg_fatal("%s: expected \"%s\" from absolute fetch, got \"%s\"",
+				 what, first, PQgetvalue(res, 0, 0));
+	PQclear(res);
+	consume_null_result(conn);
+
+	if (PQsendClosePortal(conn, portal) != 1)
+		pg_fatal("%s: PQsendClosePortal failed: %s", what, PQerrorMessage(conn));
+	consume_result_status(conn, PGRES_COMMAND_OK);
+	consume_null_result(conn);
+}
+
+/*
+ * SCROLL on plans that need replanning: SELECT 1, hash join, aggregate,
+ * parallel.
+ */
+static void
+test_cursor_bind_scroll_plans(PGconn *conn)
+{
+	PGresult   *res;
+
+	fprintf(stderr, "test_cursor_bind_scroll_plans... ");
+
+	res = PQexec(conn, "BEGIN");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("BEGIN failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	res = PQprepare(conn, "scrollplan_select1", "SELECT 1", 0, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("PREPARE failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+	check_scroll_plan(conn, "scrollplan_select1", "SELECT 1");
+
+	res = PQexec(conn, "SET enable_nestloop = off; SET enable_mergejoin = off");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("SET failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	res = PQprepare(conn, "scrollplan_hashjoin",
+					"SELECT a.g FROM generate_series(1, 5) a(g) "
+					"JOIN generate_series(1, 5) b(g) USING (g)", 0, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("PREPARE failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+	check_scroll_plan(conn, "scrollplan_hashjoin", "hash join");
+
+	res = PQprepare(conn, "scrollplan_agg",
+					"SELECT count(*) FROM generate_series(1, 5)", 0, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("PREPARE failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+	check_scroll_plan(conn, "scrollplan_agg", "aggregate");
+
+	res = PQexec(conn, "SET debug_parallel_query = on");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("SET failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	res = PQprepare(conn, "scrollplan_parallel",
+					"SELECT g FROM generate_series(1, 5) g", 0, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("PREPARE failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+	check_scroll_plan(conn, "scrollplan_parallel", "parallel");
+
+	res = PQexec(conn, "ROLLBACK");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("ROLLBACK failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	/* HOLD over a replanned plan survives commit */
+	res = PQexec(conn, "BEGIN");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("BEGIN failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	if (PQsendBindAndExecutePortal(conn, "scrollplan_hashjoin", 0, NULL, NULL,
+								   NULL, 0, "scrollplan_hold",
+								   PQ_BIND_CURSOR_SCROLL | PQ_BIND_CURSOR_HOLD,
+								   PQ_FETCH_FORWARD, 2) != 1)
+		pg_fatal("PQsendBindAndExecutePortal failed: %s", PQerrorMessage(conn));
+	confirm_fetch_result(conn, "hold+scroll before commit", 2, -1);
+
+	res = PQexec(conn, "COMMIT");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("COMMIT failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	if (PQsendExecutePortal(conn, "scrollplan_hold", PQ_FETCH_BACKWARD,
+							PQ_FETCH_ALL) != 1)
+		pg_fatal("backward fetch failed: %s", PQerrorMessage(conn));
+	confirm_fetch_result(conn, "hold+scroll after commit", 1, -1);
+
+	if (PQsendExecutePortal(conn, "scrollplan_hold", PQ_FETCH_FORWARD,
+							PQ_FETCH_ALL) != 1)
+		pg_fatal("forward fetch failed: %s", PQerrorMessage(conn));
+	confirm_fetch_result(conn, "hold+scroll forward all", 5, -1);
+
+	if (PQsendClosePortal(conn, "scrollplan_hold") != 1)
+		pg_fatal("PQsendClosePortal failed: %s", PQerrorMessage(conn));
+	consume_result_status(conn, PGRES_COMMAND_OK);
+	consume_null_result(conn);
+
+	fprintf(stderr, "ok\n");
+}
+
 static void
 test_cursor_bind_validation(PGconn *conn)
 {
@@ -2555,6 +2854,19 @@ test_cursor_bind_validation(PGconn *conn)
 									PQ_BIND_CURSOR_SCROLL | PQ_BIND_CURSOR_NO_SCROLL) != 0)
 		pg_fatal("expected rejection of SCROLL | NO_SCROLL");
 
+	/* parameter count out of range */
+	if (PQsendBindWithCursorOptions(conn, "valstmt", -1, NULL, NULL, NULL, 0,
+									"p", 0) != 0)
+		pg_fatal("expected rejection of negative parameter count");
+
+	if (PQsendBindWithCursorOptions(conn, "valstmt", 65536, NULL, NULL, NULL, 0,
+									"p", 0) != 0)
+		pg_fatal("expected rejection of too many parameters");
+
+	if (PQsendBindAndExecutePortal(conn, "valstmt", 65536, NULL, NULL, NULL, 0,
+								   "p", 0, PQ_FETCH_FORWARD, 1) != 0)
+		pg_fatal("expected rejection of too many parameters");
+
 	if (PQexitPipelineMode(conn) != 1)
 		pg_fatal("failed to exit pipeline mode: %s", PQerrorMessage(conn));
 
@@ -3000,6 +3312,29 @@ test_cursor_execute_validation(PGconn *conn)
 	if (PQexitPipelineMode(conn) != 1)
 		pg_fatal("failed to exit pipeline mode: %s", PQerrorMessage(conn));
 
+	/* INT64_MIN is rejected by the server */
+	res = PQexec(conn, "BEGIN");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("BEGIN failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
+	if (PQsendBindWithCursorOptions(conn, "exvalstmt", 0, NULL, NULL, NULL, 0,
+									"exvalportal", PQ_BIND_CURSOR_SCROLL) != 1)
+		pg_fatal("PQsendBindWithCursorOptions failed: %s", PQerrorMessage(conn));
+	consume_result_status(conn, PGRES_COMMAND_OK);
+	consume_null_result(conn);
+
+	if (PQsendExecutePortal(conn, "exvalportal", PQ_FETCH_FORWARD,
+							INT64_MIN) != 1)
+		pg_fatal("PQsendExecutePortal failed: %s", PQerrorMessage(conn));
+	consume_result_status(conn, PGRES_FATAL_ERROR);
+	consume_null_result(conn);
+
+	res = PQexec(conn, "ROLLBACK");
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pg_fatal("ROLLBACK failed: %s", PQerrorMessage(conn));
+	PQclear(res);
+
 	fprintf(stderr, "ok\n");
 }
 
@@ -3058,10 +3393,13 @@ print_test_list(void)
 {
 	printf("cancel\n");
 	printf("cursor_bind_dml\n");
+	printf("cursor_bind_for_update\n");
+	printf("cursor_bind_hold_non_select\n");
 	printf("cursor_bind_holdable\n");
 	printf("cursor_bind_holdable_scroll\n");
 	printf("cursor_bind_no_scroll\n");
 	printf("cursor_bind_scroll\n");
+	printf("cursor_bind_scroll_plans\n");
 	printf("cursor_bind_validation\n");
 	printf("cursor_bind_without_extension\n");
 	printf("cursor_execute_bind_and_fetch\n");
@@ -3178,6 +3516,10 @@ main(int argc, char **argv)
 		test_cancel(conn);
 	else if (strcmp(testname, "cursor_bind_dml") == 0)
 		test_cursor_bind_dml(conn);
+	else if (strcmp(testname, "cursor_bind_for_update") == 0)
+		test_cursor_bind_for_update(conn);
+	else if (strcmp(testname, "cursor_bind_hold_non_select") == 0)
+		test_cursor_bind_hold_non_select(conn);
 	else if (strcmp(testname, "cursor_bind_holdable") == 0)
 		test_cursor_bind_holdable(conn);
 	else if (strcmp(testname, "cursor_bind_holdable_scroll") == 0)
@@ -3186,6 +3528,8 @@ main(int argc, char **argv)
 		test_cursor_bind_no_scroll(conn);
 	else if (strcmp(testname, "cursor_bind_scroll") == 0)
 		test_cursor_bind_scroll(conn);
+	else if (strcmp(testname, "cursor_bind_scroll_plans") == 0)
+		test_cursor_bind_scroll_plans(conn);
 	else if (strcmp(testname, "cursor_bind_validation") == 0)
 		test_cursor_bind_validation(conn);
 	else if (strcmp(testname, "cursor_bind_without_extension") == 0)
-- 
2.17.1

Reply via email to