From 209d02d6e7b112b4293e22bfcd4e99cbe750259f Mon Sep 17 00:00:00 2001 From: Dave Cramer Date: Tue, 8 Sep 2026 18:05:51 +0000 Subject: [PATCH v5 1/3] Add _pq_.cursor protocol extension for cursor options in Bind This adds a new protocol extension, _pq_.cursor, letting clients specify cursor options (SCROLL, NO SCROLL, WITH HOLD) directly in the Bind message, without requiring a DECLARE CURSOR SQL command. When the extension is negotiated during connection startup, every Bind message includes a mandatory Int32 field after the result-column format codes carrying a bitmask of cursor option flags: 0x0001 SCROLL 0x0002 NO SCROLL 0x0004 WITH HOLD The field is mandatory rather than optional so that the server never has to decide from the message length whether it is present. Bind messages that cannot carry cursor options, such as the ones libpq builds for PQexecParams(), send it as zero. libpq's trace output prints the field when it is present. On the server side, backend_startup.c recognizes the _pq_.cursor startup parameter, and exec_bind_message() reads and validates the extension field, mapping the wire-level PQ_BIND_CURSOR_* flags to the internal CURSOR_OPT_* constants. SCROLL and NO SCROLL are rejected in combination, and WITH HOLD is rejected for the unnamed portal, which cannot survive its transaction. On the client side, a new libpq function PQsendBindWithCursorOptions() sends a Bind message with the cursor options field, followed by Describe but not Execute, creating a named portal that can be operated on with FETCH, MOVE, and CLOSE. PQprotocolCursorEnabled() lets applications check whether the extension was successfully negotiated. A new "protocol_cursor" connection parameter controls whether the client requests the extension. If the server rejects it via NegotiateProtocolVersion, the client silently disables the feature. Tests are added to the libpq_pipeline test module covering holdable cursors, scrollable cursors, NO SCROLL enforcement, combined HOLD+SCROLL, DML statements, client-side validation, and the case where the extension is not negotiated. Documentation is updated in both libpq.sgml (new function reference entries) and protocol.sgml (wire format and extension table). --- doc/src/sgml/libpq.sgml | 83 ++- doc/src/sgml/protocol.sgml | 38 +- src/backend/tcop/backend_startup.c | 21 +- src/backend/tcop/postgres.c | 57 ++ src/include/libpq/libpq-be.h | 1 + src/include/libpq/protocol.h | 14 + src/interfaces/libpq/exports.txt | 2 + src/interfaces/libpq/fe-connect.c | 17 + src/interfaces/libpq/fe-exec.c | 155 +++++ src/interfaces/libpq/fe-protocol3.c | 20 +- src/interfaces/libpq/fe-trace.c | 8 +- src/interfaces/libpq/libpq-fe.h | 20 + src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 532 ++++++++++++++++++ .../libpq_pipeline/t/001_libpq_pipeline.pl | 14 +- 15 files changed, 967 insertions(+), 17 deletions(-) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 68487a3954f..0311b53f238 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -3137,6 +3137,28 @@ int PQconnectionUsedGSSAPI(const PGconn *conn); + + + PQprotocolCursorEnabledPQprotocolCursorEnabled + + + Returns true (1) if the connection has successfully negotiated + the _pq_.cursor protocol extension, + false (0) if not. + + +int PQprotocolCursorEnabled(const PGconn *conn); + + + + + When this returns true, + can be used with non-zero cursor options to create scrollable or + holdable portals. Applications can use this function to implement + graceful fallback logic when the server does not support the extension. + + + @@ -5331,8 +5353,9 @@ unsigned char *PQunescapeBytea(const unsigned char *from, size_t *to_length); , , , - , and - , + , + , and + , which can be used with to duplicate the functionality of , @@ -5530,6 +5553,56 @@ int PQsendClosePortal(PGconn *conn, const char *portalName); + + PQsendBindWithCursorOptionsPQsendBindWithCursorOptions + + + + Creates a named portal from a previously prepared statement, with + the specified cursor options applied. + +int PQsendBindWithCursorOptions(PGconn *conn, + const char *stmtName, + int nParams, + const char *const *paramValues, + const int *paramLengths, + const int *paramFormats, + int resultFormat, + const char *portalName, + int cursorOptions); + + + + + The cursorOptions parameter is a bitmask of + cursor option flags. See + for the flags defined + by the _pq_.cursor extension. + + + + The portalName must be a non-empty string; + unnamed portals are rejected. The function sends a Bind message + to create the portal but does not execute it. The portal can + later be operated on with cursor commands such as FETCH, MOVE, + and CLOSE. + Returns 1 on success, 0 on failure. + + + + The _pq_.cursor protocol extension must have + been successfully negotiated during connection startup for cursor + options to take effect. This is enabled by setting the + protocol_cursor connection parameter to + 1. If the extension was not negotiated and + cursorOptions is non-zero, the function + returns 0. Passing cursorOptions as 0 is + always permitted and creates a named portal without any cursor + options, regardless of whether the extension was negotiated. + + + + PQgetResultPQgetResult @@ -5544,6 +5617,7 @@ int PQsendClosePortal(PGconn *conn, const char *portalName); , , , + , , or call, and returns it. @@ -5920,8 +5994,9 @@ int PQflush(PGconn *conn); The functions , , , - , and - also work in pipeline mode. + , + , and + also work in pipeline mode. Result processing is described below. diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index 89ac680efd5..afaca4aaa11 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -335,9 +335,26 @@ - - (No supported protocol extensions are currently defined.) - + _pq_.cursor + true + PostgreSQL 19 and later + Enables cursor options in the Bind message. + When set to true, the Bind message includes + a mandatory Int32 extension field after the result-column format + codes. The following flag values are defined: + + 0x0001 — SCROLL + 0x0002 — NO SCROLL + 0x0004 — WITH HOLD + + SCROLL and NO SCROLL are mutually exclusive. + WITH HOLD is not permitted on unnamed portals. + 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 + at all, and creates exactly the portal that a Bind message without this + extension would create. + @@ -1090,6 +1107,9 @@ SELCT 1/0; pass NULL values for them in the Bind message.) Bind also specifies the format to use for any data returned by the query; the format can be specified overall, or per-column. + If the _pq_.cursor protocol option is negotiated, + Bind includes a mandatory Int32 field with cursor options to control + portal behavior, such as creating scrollable or holdable cursors. The response is either BindComplete or ErrorResponse. @@ -4400,6 +4420,18 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" + + + Int32 (present when _pq_.cursor is negotiated) + + + Cursor option flags. See the + _pq_.cursor entry in + for defined values. + A value of 0 means no cursor options are requested. + + + diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c index 912ad7dc957..fa0bd2de748 100644 --- a/src/backend/tcop/backend_startup.c +++ b/src/backend/tcop/backend_startup.c @@ -812,11 +812,24 @@ retry: { /* * Any option beginning with _pq_. is reserved for use as a - * protocol-level option, but at present no such options are - * defined. + * protocol-level option. */ - unrecognized_protocol_options = - lappend(unrecognized_protocol_options, pstrdup(nameptr)); + if (strcmp(nameptr, "_pq_.cursor") == 0) + { + /* Enable cursor options support via Bind message */ + if (!parse_bool(valptr, &port->protocol_cursor_enabled)) + ereport(FATAL, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid value for parameter \"%s\": \"%s\"", + "_pq_.cursor", + valptr))); + } + else + { + /* Unrecognized protocol option */ + unrecognized_protocol_options = + lappend(unrecognized_protocol_options, pstrdup(nameptr)); + } } else { diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index b6bdfe213fe..524fce52dd8 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -45,6 +45,7 @@ #include "libpq/libpq.h" #include "libpq/pqformat.h" #include "libpq/pqsignal.h" +#include "libpq/protocol.h" #include "mb/pg_wchar.h" #include "mb/stringinfo_mb.h" #include "miscadmin.h" @@ -2044,6 +2045,62 @@ exec_bind_message(StringInfo input_message) rformats[i] = pq_getmsgint(input_message, 2); } + /* + * Get bind extension flags when _pq_.cursor is negotiated. + * + * The Int32 field is mandatory once the extension is active: the client + * always sends it, as 0 when no options are wanted. That way the server + * never has to guess from the message length whether it is there. + * + * The wire-level flag values (PQ_BIND_CURSOR_*) are defined in + * src/include/libpq/protocol.h independently of the server-internal + * CURSOR_OPT_* constants in parsenodes.h, so we map between the two + * representations here. + */ + if (MyProcPort->protocol_cursor_enabled) + { + int bind_ext_flags; + + bind_ext_flags = pq_getmsgint(input_message, 4); + + /* Reject any bits we don't recognize */ + if (bind_ext_flags & ~PQ_BIND_CURSOR_VALID_FLAGS) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("unrecognized bind extension flags: 0x%x", + bind_ext_flags & ~PQ_BIND_CURSOR_VALID_FLAGS))); + + /* Reject mutually exclusive SCROLL + NO_SCROLL */ + if ((bind_ext_flags & PQ_BIND_CURSOR_SCROLL) && + (bind_ext_flags & PQ_BIND_CURSOR_NO_SCROLL)) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("SCROLL and NO_SCROLL cursor options are mutually exclusive"))); + + /* Reject HOLD on unnamed portals; they cannot survive transactions */ + if ((bind_ext_flags & PQ_BIND_CURSOR_HOLD) && + portal_name[0] == '\0') + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("WITH HOLD cursor option is not allowed on unnamed portals"))); + + /* + * Map protocol flags to internal CURSOR_OPT_* values. CreatePortal + * has already applied CURSOR_OPT_NO_SCROLL, which is what a portal + * driven by Execute messages gets when the extension is not in use, + * so the flags only add to that: a portal is scrollable when, and + * only when, SCROLL is asked for. NO_SCROLL is therefore accepted + * but redundant, and all flags clear means exactly the behavior of a + * Bind message without the extension. + */ + if (bind_ext_flags & PQ_BIND_CURSOR_SCROLL) + portal->cursorOptions = (portal->cursorOptions & + ~CURSOR_OPT_NO_SCROLL) | CURSOR_OPT_SCROLL; + if (bind_ext_flags & PQ_BIND_CURSOR_NO_SCROLL) + portal->cursorOptions |= CURSOR_OPT_NO_SCROLL; + if (bind_ext_flags & PQ_BIND_CURSOR_HOLD) + portal->cursorOptions |= CURSOR_OPT_HOLD; + } pq_getmsgend(input_message); /* diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h index 921b2daa4ff..2af3296684a 100644 --- a/src/include/libpq/libpq-be.h +++ b/src/include/libpq/libpq-be.h @@ -151,6 +151,7 @@ typedef struct Port char *user_name; char *cmdline_options; List *guc_options; + bool protocol_cursor_enabled; /* _pq_.cursor option */ /* * The startup packet application name, only used here for the "connection diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..0a2271ef8a2 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -108,4 +108,18 @@ #define AUTH_REQ_SASL_FIN 12 /* Final SASL message */ #define AUTH_REQ_MAX AUTH_REQ_SASL_FIN /* maximum AUTH_REQ_* value */ +/* + * Bind message extension flags for _pq_.cursor. + * + * These values are part of the wire protocol and must not change. + * Both the server and libpq need these definitions, so they live here + * rather than in libpq-fe.h alone. + */ +#define PQ_BIND_CURSOR_SCROLL 0x0001 /* SCROLL */ +#define PQ_BIND_CURSOR_NO_SCROLL 0x0002 /* NO SCROLL */ +#define PQ_BIND_CURSOR_HOLD 0x0004 /* WITH HOLD */ +#define PQ_BIND_CURSOR_VALID_FLAGS (PQ_BIND_CURSOR_SCROLL | \ + PQ_BIND_CURSOR_NO_SCROLL | \ + PQ_BIND_CURSOR_HOLD) + #endif /* PROTOCOL_H */ diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..51d1d0c4400 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,5 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQsendBindWithCursorOptions 212 +PQprotocolCursorEnabled 213 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 2ce128da157..efe3abf4279 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -420,6 +420,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "SSL-Key-Log-File", "D", 64, offsetof(struct pg_conn, sslkeylogfile)}, + {"protocol_cursor", NULL, "0", NULL, + "Protocol-Cursor", "", 1, + offsetof(struct pg_conn, protocol_cursor)}, + /* Terminating entry --- MUST BE LAST */ {NULL, NULL, NULL, NULL, NULL, NULL, 0} @@ -3739,6 +3743,13 @@ keep_going: /* We will come back to here until there is * proceed without. */ + /* + * Set protocol_cursor_enabled based on the connection + * parameter. + */ + if (conn->protocol_cursor && conn->protocol_cursor[0] == '1') + conn->protocol_cursor_enabled = true; + /* Build the startup packet. */ startpacket = pqBuildStartupPacket3(conn, &packetlen, EnvironmentOptions); @@ -7820,6 +7831,12 @@ PQconnectionUsedGSSAPI(const PGconn *conn) return false; } +int +PQprotocolCursorEnabled(const PGconn *conn) +{ + return conn != NULL && conn->protocol_cursor_enabled; +} + int PQclientEncoding(const PGconn *conn) { diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c index 294690648bd..176b84effb8 100644 --- a/src/interfaces/libpq/fe-exec.c +++ b/src/interfaces/libpq/fe-exec.c @@ -1682,6 +1682,148 @@ PQsendQueryPrepared(PGconn *conn, resultFormat); } +/* + * PQsendBindWithCursorOptions + * Send a Bind message with cursor options, followed by Describe, but not + * Execute. This creates a named portal with the specified cursor options + * (PQ_BIND_CURSOR_* from libpq-fe.h) that can be fetched from later. + * + * Non-zero cursorOptions require the _pq_.cursor protocol + * extension; returns 0 if the extension was not negotiated. Passing + * cursorOptions as 0 creates a named portal without cursor options. + */ +int +PQsendBindWithCursorOptions(PGconn *conn, + const char *stmtName, + int nParams, + const char *const *paramValues, + const int *paramLengths, + const int *paramFormats, + int resultFormat, + const char *portalName, + int cursorOptions) +{ + PGcmdQueueEntry *entry; + + if (!PQsendQueryStart(conn, true)) + return 0; + + if (!stmtName) + { + libpq_append_conn_error(conn, "statement name is a null pointer"); + return 0; + } + + if (!portalName || portalName[0] == '\0') + { + libpq_append_conn_error(conn, "a named portal is required"); + return 0; + } + + if (cursorOptions != 0 && !conn->protocol_cursor_enabled) + { + libpq_append_conn_error(conn, + "cursor options require the _pq_.cursor protocol extension"); + return 0; + } + + if (cursorOptions & ~PQ_BIND_CURSOR_VALID_FLAGS) + { + libpq_append_conn_error(conn, + "unrecognized cursor option flags: 0x%x", + cursorOptions & ~PQ_BIND_CURSOR_VALID_FLAGS); + return 0; + } + + if ((cursorOptions & PQ_BIND_CURSOR_SCROLL) && + (cursorOptions & PQ_BIND_CURSOR_NO_SCROLL)) + { + libpq_append_conn_error(conn, + "SCROLL and NO_SCROLL cursor options are mutually exclusive"); + return 0; + } + + entry = pqAllocCmdQueueEntry(conn); + if (entry == NULL) + return 0; + + if (pqPutMsgStart(PqMsg_Bind, conn) < 0 || + pqPuts(portalName ? portalName : "", conn) < 0 || + pqPuts(stmtName, conn) < 0) + goto sendFailed; + + if (nParams > 0 && paramFormats) + { + if (pqPutInt(nParams, 2, conn) < 0) + goto sendFailed; + for (int i = 0; i < nParams; i++) + if (pqPutInt(paramFormats[i], 2, conn) < 0) + goto sendFailed; + } + else if (pqPutInt(0, 2, conn) < 0) + goto sendFailed; + + if (pqPutInt(nParams, 2, conn) < 0) + goto sendFailed; + + for (int i = 0; i < nParams; i++) + { + if (paramValues && paramValues[i]) + { + int len = paramLengths ? paramLengths[i] : strlen(paramValues[i]); + + if (pqPutInt(len, 4, conn) < 0 || + pqPutnchar(paramValues[i], len, conn) < 0) + goto sendFailed; + } + else if (pqPutInt(-1, 4, conn) < 0) + goto sendFailed; + } + + if (pqPutInt(1, 2, conn) < 0 || + pqPutInt(resultFormat, 2, conn) < 0) + goto sendFailed; + + /* Cursor options; mandatory once _pq_.cursor is negotiated */ + if (conn->protocol_cursor_enabled) + { + if (pqPutInt(cursorOptions, 4, conn) < 0) + goto sendFailed; + } + + if (pqPutMsgEnd(conn) < 0) + goto sendFailed; + + if (pqPutMsgStart(PqMsg_Describe, conn) < 0 || + pqPutc('P', conn) < 0 || + pqPuts(portalName ? portalName : "", conn) < 0 || + pqPutMsgEnd(conn) < 0) + goto sendFailed; + + /* No Execute message - portal is created but not executed */ + + if (conn->pipelineStatus == PQ_PIPELINE_OFF) + { + if (pqPutMsgStart(PqMsg_Sync, conn) < 0 || + pqPutMsgEnd(conn) < 0) + goto sendFailed; + } + + entry->queryclass = PGQUERY_DESCRIBE; + + if (pqPipelineFlush(conn) < 0) + goto sendFailed; + + /* OK, it's launched! */ + pqAppendCmdQueueEntry(conn, entry); + + return 1; + +sendFailed: + pqRecycleCmdQueueEntry(conn, entry); + return 0; +} + /* * PQsendQueryStart * Common startup code for PQsendQuery and sibling routines @@ -1883,6 +2025,19 @@ PQsendQueryGuts(PGconn *conn, if (pqPutInt(1, 2, conn) < 0 || pqPutInt(resultFormat, 2, conn)) goto sendFailed; + + /* + * The cursor options field is mandatory once _pq_.cursor has been + * negotiated, so it has to be sent even here, where no cursor options can + * be requested. This Bind always targets the unnamed portal, which + * cannot be a cursor anyway. + */ + if (conn->protocol_cursor_enabled) + { + if (pqPutInt(0, 4, conn) < 0) + goto sendFailed; + } + if (pqPutMsgEnd(conn) < 0) goto sendFailed; diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 1fd427cb9be..1b8f9ea4830 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -1512,8 +1512,9 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) conn->pversion = their_version; /* - * We don't currently request any protocol extensions, so we don't expect - * the server to reply with any either. + * The only protocol extension we might request is _pq_.cursor, so + * that is the only parameter the server can legitimately report back to + * us as unsupported. */ for (int i = 0; i < num; i++) { @@ -1526,6 +1527,17 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported unsupported parameter name without a \"%s\" prefix (\"%s\")", "_pq_.", conn->workBuffer.data); goto failure; } + + /* + * The server rejected an extension we requested. Disable the + * corresponding feature so we don't try to use it. + */ + if (strcmp(conn->workBuffer.data, "_pq_.cursor") == 0) + { + conn->protocol_cursor_enabled = false; + continue; + } + libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", conn->workBuffer.data); goto failure; @@ -2503,6 +2515,10 @@ build_startup_packet(const PGconn *conn, char *packet, if (conn->client_encoding_initial && conn->client_encoding_initial[0]) ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial); + /* Add _pq_.cursor option if enabled */ + if (conn->protocol_cursor && conn->protocol_cursor[0] == '1') + ADD_STARTUP_OPTION("_pq_.cursor", "true"); + /* Add any environment-driven GUC settings needed */ for (next_eo = options; next_eo->envName; next_eo++) { diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index 2901fa5b451..720775e83f9 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -237,7 +237,7 @@ pqTraceOutput_NotificationResponse(FILE *f, const char *message, int *cursor, bo } static void -pqTraceOutput_Bind(FILE *f, const char *message, int *cursor) +pqTraceOutput_Bind(FILE *f, const char *message, int *cursor, int length) { int nparams; @@ -264,6 +264,10 @@ pqTraceOutput_Bind(FILE *f, const char *message, int *cursor) nparams = pqTraceOutputInt16(f, message, cursor); for (int i = 0; i < nparams; i++) pqTraceOutputInt16(f, message, cursor); + + /* Cursor options, present only when _pq_.cursor is negotiated */ + if (*cursor < length + 1) + pqTraceOutputInt32(f, message, cursor, false); } static void @@ -674,7 +678,7 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) pqTraceOutput_NotificationResponse(conn->Pfdebug, message, &logCursor, regress); break; case PqMsg_Bind: - pqTraceOutput_Bind(conn->Pfdebug, message, &logCursor); + pqTraceOutput_Bind(conn->Pfdebug, message, &logCursor, length); break; case PqMsg_CopyDone: fprintf(conn->Pfdebug, "CopyDone"); diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index f51fd620b0a..85b2a5d7743 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -69,6 +69,21 @@ extern "C" /* Indicates presence of the PQAUTHDATA_OAUTH_BEARER_TOKEN_V2 authdata hook */ #define LIBPQ_HAS_OAUTH_BEARER_TOKEN_V2 1 +/* + * Bind message extension flags for _pq_.cursor. + * Canonical definitions are in src/include/libpq/protocol.h; these are + * duplicated here so that libpq clients can use them without pulling in + * server headers. + */ + +/* Flags for the _pq_.cursor extension */ +#define PQ_BIND_CURSOR_SCROLL 0x0001 /* SCROLL */ +#define PQ_BIND_CURSOR_NO_SCROLL 0x0002 /* NO SCROLL */ +#define PQ_BIND_CURSOR_HOLD 0x0004 /* WITH HOLD */ +#define PQ_BIND_CURSOR_VALID_FLAGS (PQ_BIND_CURSOR_SCROLL | \ + PQ_BIND_CURSOR_NO_SCROLL | \ + PQ_BIND_CURSOR_HOLD) + /* * Option flags for PQcopyResult */ @@ -542,6 +557,11 @@ extern int PQsendQueryPrepared(PGconn *conn, const int *paramLengths, const int *paramFormats, int resultFormat); +extern int PQsendBindWithCursorOptions(PGconn *conn, const char *stmtName, + int nParams, const char *const *paramValues, + const int *paramLengths, const int *paramFormats, + int resultFormat, const char *portalName, int cursorOptions); +extern int PQprotocolCursorEnabled(const PGconn *conn); extern int PQsetSingleRowMode(PGconn *conn); extern int PQsetChunkedRowsMode(PGconn *conn, int chunkSize); extern PGresult *PQgetResult(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 933ba0d99d5..9217c54294e 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -432,6 +432,7 @@ struct pg_conn char *scram_client_key; /* base64-encoded SCRAM client key */ char *scram_server_key; /* base64-encoded SCRAM server key */ char *sslkeylogfile; /* where should the client write ssl keylogs */ + char *protocol_cursor; /* enable _pq_.cursor option */ bool cancelRequest; /* true if this connection is used to send a * cancel request, instead of being a normal @@ -507,6 +508,7 @@ struct pg_conn int sversion; /* server version, e.g. 70401 for 7.4.1 */ bool pversion_negotiated; /* true if NegotiateProtocolVersion * was received */ + bool protocol_cursor_enabled; /* _pq_.cursor option */ bool auth_req_received; /* true if any type of auth req received */ bool password_needed; /* true if server demanded a password */ bool gssapi_used; /* true if authenticated via gssapi */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ad007038dc9..41516eb7fe5 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -2100,6 +2100,517 @@ process_result(PGconn *conn, PGresult *res, int results, int numsent) return got_error; } +/* ---- _pq_.cursor extension tests ---- */ + +/* + * Test holdable cursor: create a portal with PQ_BIND_CURSOR_HOLD via Bind, + * commit the transaction, then FETCH from the surviving portal. + */ +static void +test_cursor_bind_holdable(PGconn *conn) +{ + PGresult *res; + + fprintf(stderr, "test_cursor_bind_holdable... "); + + res = PQexec(conn, "BEGIN"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("BEGIN failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQexec(conn, "CREATE TEMP TABLE IF NOT EXISTS holdable_test(id int)"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("CREATE TABLE failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQexec(conn, "INSERT INTO holdable_test VALUES (1), (2), (3)"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("INSERT failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQprepare(conn, "holdstmt", "SELECT * FROM holdable_test", 0, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("PREPARE failed: %s", PQerrorMessage(conn)); + PQclear(res); + + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn)); + + if (PQsendBindWithCursorOptions(conn, "holdstmt", 0, NULL, NULL, NULL, 0, + "holdportal", PQ_BIND_CURSOR_HOLD) != 1) + pg_fatal("PQsendBindWithCursorOptions failed: %s", PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, "COMMIT", 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("COMMIT failed: %s", PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, "FETCH ALL FROM holdportal", 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("FETCH failed: %s", PQerrorMessage(conn)); + + if (PQsendClosePortal(conn, "holdportal") != 1) + pg_fatal("PQsendClosePortal failed: %s", PQerrorMessage(conn)); + + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + + /* Bind+Describe result */ + res = confirm_result_status(conn, PGRES_COMMAND_OK); + if (PQnfields(res) != 1) + pg_fatal("expected 1 field, got %d", PQnfields(res)); + PQclear(res); + consume_null_result(conn); + + /* COMMIT */ + consume_result_status(conn, PGRES_COMMAND_OK); + consume_null_result(conn); + + /* FETCH after commit */ + res = confirm_result_status(conn, PGRES_TUPLES_OK); + if (PQntuples(res) != 3) + pg_fatal("expected 3 rows after commit, got %d", PQntuples(res)); + PQclear(res); + consume_null_result(conn); + + /* CLOSE */ + consume_result_status(conn, PGRES_COMMAND_OK); + consume_null_result(conn); + + consume_result_status(conn, PGRES_PIPELINE_SYNC); + consume_null_result(conn); + + if (PQexitPipelineMode(conn) != 1) + pg_fatal("failed to exit pipeline mode: %s", PQerrorMessage(conn)); + + fprintf(stderr, "ok\n"); +} + +/* + * Test scroll cursor: create a portal with PQ_BIND_CURSOR_SCROLL and verify + * backward fetching works. + */ +static void +test_cursor_bind_scroll(PGconn *conn) +{ + PGresult *res; + + fprintf(stderr, "test_cursor_bind_scroll... "); + + res = PQexec(conn, "BEGIN"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("BEGIN failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQexec(conn, "CREATE TEMP TABLE IF NOT EXISTS scroll_test(id int)"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("CREATE TABLE failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQexec(conn, "INSERT INTO scroll_test VALUES (1), (2), (3)"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("INSERT failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQprepare(conn, "scrollstmt", "SELECT * FROM scroll_test ORDER BY id", 0, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("PREPARE failed: %s", PQerrorMessage(conn)); + PQclear(res); + + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn)); + + if (PQsendBindWithCursorOptions(conn, "scrollstmt", 0, NULL, NULL, NULL, 0, + "scrollportal", PQ_BIND_CURSOR_SCROLL) != 1) + pg_fatal("PQsendBindWithCursorOptions failed: %s", PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, "FETCH 2 FROM scrollportal", 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("FETCH forward failed: %s", PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, "FETCH BACKWARD 1 FROM scrollportal", 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("FETCH backward failed: %s", PQerrorMessage(conn)); + + if (PQsendClosePortal(conn, "scrollportal") != 1) + pg_fatal("PQsendClosePortal failed: %s", PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, "COMMIT", 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("COMMIT failed: %s", PQerrorMessage(conn)); + + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + + /* Bind+Describe result */ + res = confirm_result_status(conn, PGRES_COMMAND_OK); + if (PQnfields(res) != 1) + pg_fatal("expected 1 field, got %d", PQnfields(res)); + PQclear(res); + consume_null_result(conn); + + /* FETCH forward 2 */ + res = confirm_result_status(conn, PGRES_TUPLES_OK); + if (PQntuples(res) != 2) + pg_fatal("expected 2 rows from forward fetch, got %d", PQntuples(res)); + PQclear(res); + consume_null_result(conn); + + /* FETCH backward 1 */ + res = confirm_result_status(conn, PGRES_TUPLES_OK); + if (PQntuples(res) != 1) + pg_fatal("expected 1 row from backward fetch, got %d", PQntuples(res)); + if (strcmp(PQgetvalue(res, 0, 0), "1") != 0) + pg_fatal("expected value '1' from backward fetch, got '%s'", PQgetvalue(res, 0, 0)); + PQclear(res); + consume_null_result(conn); + + /* CLOSE */ + consume_result_status(conn, PGRES_COMMAND_OK); + consume_null_result(conn); + + /* COMMIT */ + consume_result_status(conn, PGRES_COMMAND_OK); + consume_null_result(conn); + + consume_result_status(conn, PGRES_PIPELINE_SYNC); + consume_null_result(conn); + + if (PQexitPipelineMode(conn) != 1) + pg_fatal("failed to exit pipeline mode: %s", PQerrorMessage(conn)); + + fprintf(stderr, "ok\n"); +} + +/* + * Test no-scroll cursor: create a portal with PQ_BIND_CURSOR_NO_SCROLL and + * verify backward fetching is rejected. + */ +static void +test_cursor_bind_no_scroll(PGconn *conn) +{ + PGresult *res; + + fprintf(stderr, "test_cursor_bind_no_scroll... "); + + res = PQexec(conn, "BEGIN"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("BEGIN failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQexec(conn, "CREATE TEMP TABLE IF NOT EXISTS noscroll_test(id int)"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("CREATE TABLE failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQexec(conn, "INSERT INTO noscroll_test VALUES (1), (2), (3)"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("INSERT failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQprepare(conn, "noscrollstmt", "SELECT * FROM noscroll_test ORDER BY id", 0, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("PREPARE failed: %s", PQerrorMessage(conn)); + PQclear(res); + + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn)); + + if (PQsendBindWithCursorOptions(conn, "noscrollstmt", 0, NULL, NULL, NULL, 0, + "noscrollportal", PQ_BIND_CURSOR_NO_SCROLL) != 1) + pg_fatal("PQsendBindWithCursorOptions failed: %s", PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, "FETCH 1 FROM noscrollportal", 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("FETCH forward failed: %s", PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, "FETCH BACKWARD 1 FROM noscrollportal", 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("FETCH backward send failed: %s", PQerrorMessage(conn)); + + if (PQsendClosePortal(conn, "noscrollportal") != 1) + pg_fatal("PQsendClosePortal failed: %s", PQerrorMessage(conn)); + + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + + /* Bind+Describe result */ + res = confirm_result_status(conn, PGRES_COMMAND_OK); + if (PQnfields(res) != 1) + pg_fatal("expected 1 field, got %d", PQnfields(res)); + PQclear(res); + consume_null_result(conn); + + /* FETCH forward 1 - should succeed */ + res = confirm_result_status(conn, PGRES_TUPLES_OK); + if (PQntuples(res) != 1) + pg_fatal("expected 1 row from forward fetch, got %d", PQntuples(res)); + PQclear(res); + consume_null_result(conn); + + /* FETCH backward - should fail */ + consume_result_status(conn, PGRES_FATAL_ERROR); + consume_null_result(conn); + + /* CLOSE - pipeline is aborted after the error */ + consume_result_status(conn, PGRES_PIPELINE_ABORTED); + consume_null_result(conn); + + consume_result_status(conn, PGRES_PIPELINE_SYNC); + consume_null_result(conn); + + if (PQexitPipelineMode(conn) != 1) + pg_fatal("failed to exit pipeline mode: %s", PQerrorMessage(conn)); + + res = PQexec(conn, "ROLLBACK"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("ROLLBACK failed: %s", PQerrorMessage(conn)); + PQclear(res); + + fprintf(stderr, "ok\n"); +} + +/* + * Test combined holdable + scrollable portal. + */ +static void +test_cursor_bind_holdable_scroll(PGconn *conn) +{ + PGresult *res; + + fprintf(stderr, "test_cursor_bind_holdable_scroll... "); + + res = PQexec(conn, "BEGIN"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("BEGIN failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQexec(conn, "CREATE TEMP TABLE IF NOT EXISTS holdscroll_test(id int)"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("CREATE TABLE failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQexec(conn, "INSERT INTO holdscroll_test VALUES (1), (2), (3)"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("INSERT failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQprepare(conn, "holdscrollstmt", + "SELECT * FROM holdscroll_test ORDER BY id", 0, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("PREPARE failed: %s", PQerrorMessage(conn)); + PQclear(res); + + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn)); + + if (PQsendBindWithCursorOptions(conn, "holdscrollstmt", 0, NULL, NULL, NULL, 0, + "holdscrollportal", + PQ_BIND_CURSOR_HOLD | PQ_BIND_CURSOR_SCROLL) != 1) + pg_fatal("PQsendBindWithCursorOptions failed: %s", PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, "COMMIT", 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("COMMIT failed: %s", PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, "FETCH 2 FROM holdscrollportal", + 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("FETCH forward failed: %s", PQerrorMessage(conn)); + + if (PQsendQueryParams(conn, "FETCH BACKWARD 1 FROM holdscrollportal", + 0, NULL, NULL, NULL, NULL, 0) != 1) + pg_fatal("FETCH backward failed: %s", PQerrorMessage(conn)); + + if (PQsendClosePortal(conn, "holdscrollportal") != 1) + pg_fatal("PQsendClosePortal failed: %s", PQerrorMessage(conn)); + + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + + /* Bind+Describe */ + res = confirm_result_status(conn, PGRES_COMMAND_OK); + if (PQnfields(res) != 1) + pg_fatal("expected 1 field, got %d", PQnfields(res)); + PQclear(res); + consume_null_result(conn); + + /* COMMIT */ + consume_result_status(conn, PGRES_COMMAND_OK); + consume_null_result(conn); + + /* FETCH forward 2 */ + res = confirm_result_status(conn, PGRES_TUPLES_OK); + if (PQntuples(res) != 2) + pg_fatal("expected 2 rows from forward fetch, got %d", PQntuples(res)); + PQclear(res); + consume_null_result(conn); + + /* FETCH backward 1 */ + res = confirm_result_status(conn, PGRES_TUPLES_OK); + if (PQntuples(res) != 1) + pg_fatal("expected 1 row from backward fetch, got %d", PQntuples(res)); + if (strcmp(PQgetvalue(res, 0, 0), "1") != 0) + pg_fatal("expected value '1' from backward fetch, got '%s'", + PQgetvalue(res, 0, 0)); + PQclear(res); + consume_null_result(conn); + + /* CLOSE */ + consume_result_status(conn, PGRES_COMMAND_OK); + consume_null_result(conn); + + consume_result_status(conn, PGRES_PIPELINE_SYNC); + consume_null_result(conn); + + if (PQexitPipelineMode(conn) != 1) + pg_fatal("failed to exit pipeline mode: %s", PQerrorMessage(conn)); + + fprintf(stderr, "ok\n"); +} + +/* + * Test cursor options on a DML statement are harmlessly ignored. + */ +static void +test_cursor_bind_dml(PGconn *conn) +{ + PGresult *res; + + fprintf(stderr, "test_cursor_bind_dml... "); + + res = PQexec(conn, "CREATE TEMP TABLE IF NOT EXISTS dml_test(id int)"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("CREATE TABLE failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQprepare(conn, "dmlstmt", + "INSERT INTO dml_test VALUES (1), (2), (3)", 0, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("PREPARE failed: %s", PQerrorMessage(conn)); + PQclear(res); + + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn)); + + if (PQsendBindWithCursorOptions(conn, "dmlstmt", 0, NULL, NULL, NULL, 0, + "dmlportal", PQ_BIND_CURSOR_SCROLL) != 1) + pg_fatal("PQsendBindWithCursorOptions failed: %s", PQerrorMessage(conn)); + + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + + res = confirm_result_status(conn, PGRES_COMMAND_OK); + PQclear(res); + consume_null_result(conn); + + consume_result_status(conn, PGRES_PIPELINE_SYNC); + consume_null_result(conn); + + if (PQexitPipelineMode(conn) != 1) + pg_fatal("failed to exit pipeline mode: %s", PQerrorMessage(conn)); + + res = PQexec(conn, "SELECT count(*) FROM dml_test"); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pg_fatal("SELECT count failed: %s", PQerrorMessage(conn)); + if (strcmp(PQgetvalue(res, 0, 0), "0") != 0) + pg_fatal("expected 0 rows (Bind+Describe doesn't execute), got %s", + PQgetvalue(res, 0, 0)); + PQclear(res); + + fprintf(stderr, "ok\n"); +} + +/* + * Test client-side validation of cursor bind options. + */ +static void +test_cursor_bind_validation(PGconn *conn) +{ + PGresult *res; + + fprintf(stderr, "test_cursor_bind_validation... "); + + res = PQprepare(conn, "valstmt", "SELECT 1", 0, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("PREPARE failed: %s", PQerrorMessage(conn)); + PQclear(res); + + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn)); + + /* Empty portal name rejected */ + if (PQsendBindWithCursorOptions(conn, "valstmt", 0, NULL, NULL, NULL, 0, + "", PQ_BIND_CURSOR_HOLD) != 0) + pg_fatal("expected rejection of empty portal name"); + + /* NULL portal name rejected */ + if (PQsendBindWithCursorOptions(conn, "valstmt", 0, NULL, NULL, NULL, 0, + NULL, PQ_BIND_CURSOR_HOLD) != 0) + pg_fatal("expected rejection of NULL portal name"); + + /* Invalid flag bits rejected */ + if (PQsendBindWithCursorOptions(conn, "valstmt", 0, NULL, NULL, NULL, 0, + "p", 0x0008) != 0) + pg_fatal("expected rejection of invalid flags"); + + /* Mixed valid+invalid flags rejected */ + if (PQsendBindWithCursorOptions(conn, "valstmt", 0, NULL, NULL, NULL, 0, + "p", PQ_BIND_CURSOR_HOLD | 0x0100) != 0) + pg_fatal("expected rejection of mixed invalid flags"); + + /* SCROLL | NO_SCROLL rejected */ + if (PQsendBindWithCursorOptions(conn, "valstmt", 0, NULL, NULL, NULL, 0, + "p", + PQ_BIND_CURSOR_SCROLL | PQ_BIND_CURSOR_NO_SCROLL) != 0) + pg_fatal("expected rejection of SCROLL | NO_SCROLL"); + + if (PQexitPipelineMode(conn) != 1) + pg_fatal("failed to exit pipeline mode: %s", PQerrorMessage(conn)); + + fprintf(stderr, "ok\n"); +} + +/* + * Test that cursor options are rejected when _pq_.cursor is not + * negotiated. This test must be run with a connection that does NOT + * have protocol_cursor=1. + */ +static void +test_cursor_bind_without_extension(PGconn *conn) +{ + PGresult *res; + + fprintf(stderr, "test_cursor_bind_without_extension... "); + + if (PQprotocolCursorEnabled(conn) != 0) + pg_fatal("expected PQprotocolCursorEnabled to return false"); + + res = PQprepare(conn, "noextstmt", "SELECT 1", 0, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("PREPARE failed: %s", PQerrorMessage(conn)); + PQclear(res); + + if (PQenterPipelineMode(conn) != 1) + pg_fatal("failed to enter pipeline mode: %s", PQerrorMessage(conn)); + + /* Non-zero cursorOptions rejected when extension is disabled */ + if (PQsendBindWithCursorOptions(conn, "noextstmt", 0, NULL, NULL, NULL, 0, + "noextportal", PQ_BIND_CURSOR_HOLD) != 0) + pg_fatal("expected rejection of cursor options without extension"); + + /* Zero cursorOptions should still succeed */ + if (PQsendBindWithCursorOptions(conn, "noextstmt", 0, NULL, NULL, NULL, 0, + "noextportal", 0) != 1) + pg_fatal("PQsendBindWithCursorOptions with zero options failed: %s", + PQerrorMessage(conn)); + + if (PQpipelineSync(conn) != 1) + pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); + + res = confirm_result_status(conn, PGRES_COMMAND_OK); + PQclear(res); + consume_null_result(conn); + + consume_result_status(conn, PGRES_PIPELINE_SYNC); + consume_null_result(conn); + + if (PQexitPipelineMode(conn) != 1) + pg_fatal("failed to exit pipeline mode: %s", PQerrorMessage(conn)); + + fprintf(stderr, "ok\n"); +} + static void usage(const char *progname) @@ -2117,6 +2628,13 @@ static void print_test_list(void) { printf("cancel\n"); + printf("cursor_bind_dml\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_validation\n"); + printf("cursor_bind_without_extension\n"); printf("disallowed_in_pipeline\n"); printf("multi_pipelines\n"); printf("nosync\n"); @@ -2223,6 +2741,20 @@ main(int argc, char **argv) if (strcmp(testname, "cancel") == 0) test_cancel(conn); + else if (strcmp(testname, "cursor_bind_dml") == 0) + test_cursor_bind_dml(conn); + else if (strcmp(testname, "cursor_bind_holdable") == 0) + test_cursor_bind_holdable(conn); + else if (strcmp(testname, "cursor_bind_holdable_scroll") == 0) + test_cursor_bind_holdable_scroll(conn); + else if (strcmp(testname, "cursor_bind_no_scroll") == 0) + 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_validation") == 0) + test_cursor_bind_validation(conn); + else if (strcmp(testname, "cursor_bind_without_extension") == 0) + test_cursor_bind_without_extension(conn); else if (strcmp(testname, "disallowed_in_pipeline") == 0) test_disallowed_in_pipeline(conn); else if (strcmp(testname, "multi_pipelines") == 0) diff --git a/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl b/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl index c74fda8aa37..91c8af42f78 100644 --- a/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl +++ b/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl @@ -50,10 +50,20 @@ for my $testname (@tests) } # Execute the test using the latest protocol version. + # cursor_* tests need the protocol_cursor connection parameter. + my $connstr = $node->connstr('postgres') . " max_protocol_version=latest"; + if ($testname =~ /_without_extension$/) + { + $connstr = $node->connstr('postgres') . " protocol_cursor=0"; + } + elsif ($testname =~ /^cursor_/) + { + $connstr .= " protocol_cursor=1"; + } + $node->command_ok( [ - 'libpq_pipeline', @extraargs, $testname, - $node->connstr('postgres') . " max_protocol_version=latest" + 'libpq_pipeline', @extraargs, $testname, $connstr ], "libpq_pipeline $testname");