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> — 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
